narzoul / DDrawCompat

DirectDraw and Direct3D 1-7 compatibility, performance and visual enhancements for Windows Vista, 7, 8, 10 and 11
BSD Zero Clause License
952 stars 70 forks source link

Exclusive fullscreen broken in Windows 8.1 with multiple monitors in extended mode #28

Closed UCyborg closed 4 years ago

UCyborg commented 6 years ago

There is an odd Windows 8.1 specific bug with ddraw.dll based stuff, calling SetAppCompatData(12, 0) to enable real fullscreen makes any fullscreen DirectDraw application crap out when you have more than one monitor enabled, depending on application and graphics driver combination, the application will either bail out reporting an error like DDERR_GENERIC returned from one of DDraw/D3D APIs, crash or be practically unusable with corrupted graphics with all monitors blinking, see this video.

The bug is related to 8.1's Desktop Window Manager. Suspending winlogon.exe and terminating dwm.exe makes fullscreen work without the need to disable secondary monitor, although DDraw applications aren't restored properly after tabbing away (blank window), along with other undesired side effects that come with it. Even if it worked properly, it's an ugly workaround. Any ideas if this could be fixed somehow?

elishacloud commented 6 years ago

@UCyborg, I created a wrapper using DDrawCompat and other tools. It can allow you to disable the calling of SetAppCompatData(12, 0). You can find the tool here.

Just add the following line into the .ini file to disable SetAppCompatData(12, 0):

DisableMaxWindowedMode   = 0
UCyborg commented 6 years ago

I know you can just disable call to SetAppCompatData and lose real fullscreen in the process, but I'm wondering if someone would be able to figure out the real solution to the problem. The bug could be hiding in gdi32.dll or maybe even in one of DWM's DLLs.

I tried running with Windows 8.0's ddraw.dll and apparently the bug is not in that DLL, the other DLLs can't just be replaced without breaking the system. I have no idea how to debug this. It's strange that the issue only occurs with DirectDraw, but not newer APIs (DX8+).

There is another problem which isn't addressed by a single ddraw wrapper AFAIK. It affects Windows 8.0, 8.1 and 10. There is 3 second delay when switching away or back to fullscreen ddraw application if it's running at the same resolution and refresh rate as the desktop. The problem is waiting on event object DWM_DX_FULLSCREEN_TRANSITION_EVENT, which times out after 3 seconds because it isn't signaled in such scenario. If you prevent access to the said event object, it speeds up such transitions significantly, but not all drivers take it well and you may end up with garbled screen or crash.

narzoul commented 6 years ago

I'm not sure if I can do much to help since I no longer have a Windows 8 test system. Could you compile a debug version of DDrawCompat and attach the logs from that? At least we can try to compare that to the Windows 10 logs and see if there are any apparent differences.

Is the issue relevant only to DDrawCompat, or do you also get it if you just apply "DXPrimaryEmulation -DisableMaxWindowedMode" via Compatibility Administrator?

There is another problem which isn't addressed by a single ddraw wrapper AFAIK. It affects Windows 8.0, 8.1 and 10. There is 3 second delay when switching away or back to fullscreen ddraw application if it's running at the same resolution and refresh rate as the desktop. The problem is waiting on event object DWM_DX_FULLSCREEN_TRANSITION_EVENT, which times out after 3 seconds because it isn't signaled in such scenario. If you prevent access to the said event object, it speeds up such transitions significantly, but not all drivers take it well and you may end up with garbled screen or crash.

There is a similar optimization in the experimental version already, do you mean it's not working or it has side effects? I didn't notice any problems with it so far.

UCyborg commented 6 years ago

Pardon, I was going off the old information, the delay is definitely taken care of in the current experimental version. Haven't encountered any issues with it neither. It just came to my mind as I was writing and I haven't checked newer version at the time.

Regarding Windows 8.1 and multi-monitor problem, yes, it does occur using only "DXPrimaryEmulation -DisableMaxWindowedMode". One thing I noticed, calls to IDirectDrawSurface::Flip fail with DDERR_UNSUPPORTED. Same fail locking the primary surface. What's interesting, right after switching to fullscreen, first frame is displayed correctly, which can be nicely seen by setting breakpoint after the call to Flip.

Another workaround that works is disabling composition by suspending winlogon.exe and terminating dwm.exe. User must also prevent the system from loading Windows.UI.Search.dll, otherwise the right portion of the primary screen will be unusable, being obscured by Metro search interface window placeholder.

Log with one monitor: ddraw.log Log with 2 monitors: ddraw-2.log

narzoul commented 6 years ago

Thanks! From your logs, the first frame indeed appears to be presented correctly (look for _D3DDDI_DEVICEFUNCS::pfnPresent1 instead of Flip, as Flip is called from the wrapper internally in this case and doesn't show up in the log). The correct sequence of events should be that the runtime calls pfnPresent1 in the user mode display driver, which calls back to the runtime via pfnPresentCb, which should finally call D3DKMTPresent. This last step appears to be missing from the second call to pfnPresent1 with 2 monitors. Because pfnPresentCb is implemented by ddraw.dll, it must have detected some problem at this point and aborted the operation (but curiously still returned a success code).

The next call into DirectDraw resets (reopens) the device, which means all video memory surfaces are destroyed and the app must restore them. This may be why the later calls to Lock/Flip are failing.

As far as I've seen, such a device reset mostly happens if DirectDraw detects that the display mode has changed. It does this by calling GdiEntry13, which returns a counter that is incremented by the system every time the display mode changes.

To check if this is the problem, we should log the value of GdiEntry13 in some places. Please modify the two threadSafeFunc templates in Common/CompatVtable.h as follows:

  1. Insert this as the first line: static auto GdiEntry13 = reinterpret_cast<ULONG(WINAPI*)()>(GetProcAddress(GetModuleHandle("gdi32"), "GdiEntry13"));
  2. Move the DDraw::ScopedThreadLock lock; line directly below the Compat::LogEnter call.
  3. Modify the LogEnter and LogLeave call parameters to funcName, GdiEntry13(), firstParam, params....

Attach the logs from the 2 monitor case again.

Is the game resolution matching the desktop resolution? In that case the aforementioned DWM_DX_FULLSCREEN_TRANSITION_EVENT optimization may be interfering as well. Please repeat the test also with different game/desktop resolution.

Finally, we can probably rule out a display mode change issue by removing the SetDisplayMode call altogether. For this, make sure the desktop and game resolutions are matching again, and modify the return statement in DDraw/DirectDraw.cpp HRESULT STDMETHODCALLTYPE DirectDraw<TDirectDraw>::SetDisplayMode to simply return DD_OK. You should probably test this with single monitor setup too, just to make sure it doesn't break the game. Attach the logs from the 2 monitor setup again.

UCyborg commented 6 years ago

Tested all scenarios in the same order you wrote. Unless noted, both monitors are active and the game is ran at the same display settings as the desktop (1920 x 1080, 32 bpp, 59 Hz):

After adding GdiEntry13 logging: ddraw-1.log Repeated the same test, but with 1440x1080 in-game resolution, desktop remained at 1920x1080: ddraw-2.log After stubbing out SetDisplayMode: ddraw-3.log Same as the previous test, but with a single monitor: ddraw-4.log

The last two tests are interesting. The game works fine with a single monitor active after stubbing out SetDisplayMode. But with both monitors, it is clearly seen that the switch to exclusive mode fails. I ended up with fully white game window covering the screen with taskbar on top. Usually, you get this madness, which stops when you get the game to lose focus. Anyway, I could normally interact with the taskbar and other windows on both screens. The game was unresponsive though, just menu music was playing so I had to terminate its process. In other tests, Alt + F4 did the trick.

I wrote in the OP that the game doesn't come back after tabbing back in it with composition disabled. I either did something different or something else changed on my system; it doesn't happen now.

narzoul commented 6 years ago

Thanks again! I think we're making progress. In the 3rd test, the second call to pfnPresent1 now actually results in calling D3DKMTPresent, but it fails with 0xc01e0106 (STATUS_GRAPHICS_ALLOCATION_INVALID). According to MS docs, this means "The primary surface handle was invalidated because of a display mode change". Of course, based on the GdiEntry13 logs we can see that no such thing actually happened. But now I realize what could be the important difference between the first and second presentation.

The first pfnPresent1 call uses the back buffer surface allocation, while the second one (due to the buffer swap) will use the (former) front buffer allocation. While the back buffer is created freshly when DirectDraw creates the primary surface chain, for the front buffer it just opens the shared primary surface "created by the Microsoft DirectX graphics kernel subsystem (Dxgkrnl.sys) every time the display mode changes" (quoted from the docs of D3DKMTGetSharedPrimaryHandle). It may be that your system only has an issue with presenting to this shared surface, at least in multiple monitor setups.

This is going to be a bit of a hack (again), but for testing purposes we can try to swap back the surface resource handles after each flip so that pfnPresent1 will always end up using the same newly allocated back buffer surface. This doesn't seem to cause any problems on my system, so hopefully it'll work on yours too.

In DDraw/RealPrimarySurface.cpp, you'll find two calls to g_frontBuffer->Flip, one in updateNow and the other in RealPrimarySurface::flip. Insert the following code after both of those calls: std::swap(reinterpret_cast<HANDLE**>(g_frontBuffer.get())[1][2], reinterpret_cast<HANDLE**>(g_backBuffer.get())[1][2]);

Please try it with and without the SetDisplayMode stub.

narzoul commented 6 years ago

Are you sure Direct3D 8 and 9 are not affected? I just tested AquaNox 1 and 2 (GOG versions) and they also use the shared primary surface for flip mode presentation, at least if you enable real full-screen mode properly. For AquaNox 1 (D3D8) I used your modified d3d8.dll from Vogons. For AquaNox 2 (D3D9) I used the Compatibility Administrator's DXMaximizedWindowedMode shim with the command-line parameter set to Disable (note: no dashes).

Which games have you tested?

UCyborg commented 6 years ago

Good news, your hack works! Changing resolution in-game doesn't reveal any additional issues. Yes, the multi-monitor issue is exclusive to anything driven through ddraw.dll on Windows 8.1. Graphics card doesn't seem to matter neither, got it on both older AMD Radeon HD 4890 as well as on newer NVIDIA GeForce GTX 750 Ti. Same story with VMware's virtual graphics card. It does not occur with games that render through d3d8.dll and d3d9.dll. I can play Max Payne, Max Payne 2, Mafia, GTA III, GTA: Vice City, GTA: San Andreas, The Suffering The Suffering: Ties That Bind with both monitors enabled without issues. d3d9.dll driven Call of Duty games work too. BTW, the multi-monitor problem with ddraw never occurs with the option for duplicating displays.

Windows 8.1 and earlier don't enable maximized windowed mode by default for D3D8 and D3D9, although there are exceptions; you have to check with Compatibility Administrator. The mentioned GTA games come to mind and the reason for it is usage of ddraw for intro cinematics.

I'm not 100% certain about Windows 10's behavior for D3D9, my guess is that it enables maximized windowed mode only if the system is considered Game DVR capable. Which in practice probably includes most machines suitable for gaming, assuming from the behavior on VMware (no taskbar thumbnail preview by default, which should indicate real fullscreen). Didn't know about the parameter you mentioned for use with DXMaximizedWindowedMode shim, thanks for that. Last time I checked, the checkbox for disabling fullscreen optimizations in application's properties Compatibility tab worked as well. It'd be nice if it was also effective for D3D8 and DDraw.

I noticed on my end that DDrawCompat introduces some performance penalty, especially the current experimental version. Nothing to do with the new hack of course. I did compile the release version. It makes Drakan quite choppy, with frame drops down to 20s or 30s when it would normally go above 60 FPS. The penalty is significantly smaller with stable version, from 33 FPS to 28 FPS in one scene with more polygons.

A little background on that old forgotten game. Drakan: Order of the Flame's issues related to rendering API usage date back to Windows XP era. I figured it had to do with DDSCAPS_MIPMAP flag passed to CreateSurface method, which is used to compose the menu background when you pause the game. At one point, the drivers must have started to refuse to accept that flag because that surface's dimensions usually aren't square power-of-two, resulting in black background and crashing after difficulty selection due to zero pointer dereference. The dimensions match the screen resolution. At the time, I was debugging the game on Windows 7, which ddraw.dll contains error messages. From my memory, the function that takes error string as parameter is stubbed out, so messages don't appear anywhere, but they can be encountered while stepping through code when something goes wrong. That error message helped to understand what exactly goes wrong. I check some random flag in one variable in a function that calls CreateSurface to determine whether DDSCAPS_MIPMAP flag should be used. Seems to work without breaking mipmapping, which can only be tried out by editing the game files with the Level Editor as the mipmaps aren't used by the stock the game. That and many other issues are taken care of using AiO Patch.

So, with stock DDraw/D3D, other than the oddities related to DWM on newer systems, one being taken care of by calling SetAppCompat data in the patch, the only things going wrong as far as I'm aware occur with (certain?) NVIDIA drivers. Forcing anti-aliaisng (MSAA) results in significant performance drop in menus and the screenshot feature outputs black screenshot (interestingly, the latter only occurs when color depth is set to 32bpp). The game also hangs with certain drivers on Windows 10 when tabbing out while in fullscreen mode or on exit. Doesn't happen on 347.88, but it does with 368.81. Don't know about newer drivers, I encountered system stability issues with them. What helps with the hang is hitting Ctrl + Alt + Delete. Just opening that screen and closing it gets things moving again. When I had Geforce4 MX 440 in previous PC, I had to stop updating drivers at one point as well because they only slowed games down.

I'm glad you figured out the workaround for the multi-monitor problem. I'd say it helps make experience better for those swearing by Windows 8.1.

narzoul commented 5 years ago

Should be fixed in latest experimental release, though I couldn't test it. Can you please confirm if it's fixed?

UCyborg commented 5 years ago

I don't use Windows 8.1 anymore due to this and other unrelated issues. I tried it in VMware virtual machine with Windows 8.1 and fullscreen mode doesn't work there; Drakan falls back to windowed mode, Half-Life complains that selected D3D mode is not supported by the video card.

On my main Windows 10 install, your ddraw.dll just makes the process hang at startup. Doesn't matter which game I try. BTW, they fixed the bug with DWM_DX_FULLSCREEN_TRANSITION_EVENT on Windows 10 version 1809 (Build 17763).

ddraw-win8.log ddraw-win10.log

narzoul commented 5 years ago

I see nothing particularly wrong from the log apart from the fact that the process stopped in the middle of hooking the user-mode display driver. I see you have an NVIDIA GPU, which I haven't been able to test in quite some time now, so it's possible it doesn't work well currently. Do you have another GPU (even integrated) you could test with to see if those work on the same system?

I went ahead and added a full debug version to the experimental release. If you can assist me in debugging this problem so I don't have to replace my AMD GPU with an old NVIDIA one, I would really appreciate it.

Once the process hangs, right click on it in Task Manager's Details tab and select the "Analyse wait chain" option. Note down any info it might give. Then right click on the process again and select "Create dump file". It will tell you the location when it's finished (usually %TEMP%). Afterwards you can terminate the process.

If you have Visual Studio or some other debugger installed and you know how to extract the stack traces of all threads from the dump file, please do so. If not and you can upload the whole file somewhere, I can do it myself.

UCyborg commented 5 years ago

I have an older integrated GPU on the same system (GeForce 8200), currently disabled. I'm worried if installing drivers for it might screw up the drivers for GeForce GTX 750 Ti. It looks like the hanging is indeed related to handling of the user-mode display driver. I tried it on Windows 10 in a virtual machine and results there are the same as on Windows 8.1, it starts, just the fullscreen mode doesn't work.

On my main install, "Analyze wait chain" option comes up empty. I did capture the process dump.

https://drive.google.com/open?id=1qoHVVBduGzzJBJCfjWCxGhD0kuczZjic

narzoul commented 5 years ago

Ah, sorry, I should have been clearer in my instructions. By "full debug version" I meant the ddraw-debug.zip file, not ddraw-debuglogs.zip. The latter is still a release build, it just contains the debug logs, but not debug info. Please take a new process dump with the debug version (ddraw-debug.zip), if possible. Also please include the log file from the same run.

narzoul commented 5 years ago

By the way, this is quite possibly unrelated, but while I was checking the stack traces, I was curios about the LdrpDrainWorkQueue function and found this article: https://threatvector.cylance.com/en_us/home/windows-10-parallel-loading-breakdown.html

Apparently this Parallel Loading feature can be disabled by a registry entry on a per-process basis. You only need to add a MaxLoaderThreads DWORD value 1 at HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\<image.exe>\. You should see an example under the chrome.exe entry at the above path. Could you test if adding a similar entry for Drakan.exe changes anything?

UCyborg commented 5 years ago

Adding MaxLoaderThreads=1 for Drakan.exe doesn't change anything. I created a new dump with the correct version of ddraw.dll.

https://drive.google.com/open?id=1qoHVVBduGzzJBJCfjWCxGhD0kuczZjic ddraw.log

narzoul commented 5 years ago

Should be fixed in latest experimental release, please check.

UCyborg commented 5 years ago

Yup, no more hanging. BTW, I've got logs from VMware Win8 virtual machine, where it fails to fullscreen. I suspect if it worked properly with poor VMware drivers, it may also work correctly on real hardware.

ddraw-win8.log