A little while after I wrote the titlebar fix for THPS 2 described in Tony Hawk’s Pro Cleanup I got annoyed by how every time I launch the game I have to sit through a display mode switch, since even with community mods and patches there’s no way to get the game to run at the same native resolution as my 4K display. Thinking about it, there’s nothing stopping the OS or another compatibility layer from sitting between the app and the windowing system, making the app think it has exclusive fullscreen while actually rendering to a texture that can be scaled and composited to the current native resolution without a mode switch. Such a compatibility layer didn’t exist though, so to the surprise of no one who’s familiar with where these posts usually go I ended up writing one.
I started by looking up what API the game uses, and it turned out to be using Direct3D 7. Back then the D3D API wasn’t entirely fleshed out and it was intermingled with the 2D blit-based DirectDraw API, both living in ddraw.dll and sharing interfaces. Finding SDKs, documentation, or sample code was difficuly but luckily Michael Poulain shared a link to a DirectX SDK archive where I found the older SDKs I was after.
The next step was writing an interception DLL. It was mostly straightforward, overriding DirectDrawCreate()
and DirectDrawCreateEX()
and returning wrapper objects for the COM interfaces involved. The API’s age was showing though, as many of the structures involved were full of unions and bitflags and trying to mentally parse what the contents in the Watch window means was exhausting - I could really feel how far we’ve come as I compared this to D3D11 / D3D12, but that a whole other topic. For now I started intercepting the DirectDraw and Direct3D 7 API calls, and I was able to detect the mode set request and create a D3D12 swapchain of the requested resolution, and later detect the creation of the primary surface. Most of the time was spent realizing and dealing with older API requirements, and filtering out textures from any COM wrapping and fixing the occasional crash here and there due to binary layout incompatibilities caused by the interception layer.
Once the primary surface was identified the rest was simple - when the app asks to present that surface, I’d have my wrapper blit from it to the swapchain’s presentation target or an intermediate texture, depending on whether post-processing or aspect ration correction were needed. Funny enough, DXGI supports aspect-ratio-correct scaling but only for UWP apps, so in some cases I needed to blit to an intermediate texture then draw it over an aspect-correct screen-facing primitive. It’s a weird restriction and I don’t see any technical reason for it, but if you do please let me know.
Surprisingly enough, this worked just as expected - instead of scaling the screen’s display resolution to match the game’s window, the game would now scale the window to match the display resolution, render to an internal surface with the desired resolution then blit and scale that to the actual resolution. You never realize how annoying the mode switch is until you can avoid it entirely - you can now get into and out of the game in seconds (it takes less time to start the game and get to playing the Hangar level than it does to merely switch resolutions!) and you get to keep all your open windows where they are! I wonder why the built-in app compat doesn’t do this - Microsoft seems to agree that mode switching isn’t good UX, and UWP apps are prevented from doing it which is, as much as I hate everything related to UWP, a solid win.
The patch consists of a modified executable and a wrapper DirectDraw / Direct3D 7 DLL that should be placed together in the game directory to replace the existing executables. There’s also a Reddit discussion and as usual if you have any issues with this please reach out to me.