Fixing Harry Potter 2 on Linux (Wine)

If you just want the fixed files, scroll to the bottom of this page.

Harry Potter looking at Tux on his bed


One of the most viewed articles on this site is my fix for HP2 on modern Windows, a fix based on WineD3D, a component of Wine, the emulator that allows you to run Windows apps on GNU/Linux systems. Since I've been using WineD3D to fix this game on Windows for years now, I expected the game to run out of the box on Linux, but instead, I was greeted with a nasty surprise.

The launcher is black and the only visible button is Quit. Well, I'm not a quitter so let's try to fix it.

Investigating the problem

The first thing that I tried were versions of Wine, but the problem was present on stable, development, staging and even Proton, so no luck.

Then I thought, since I'm using a high resolution display, maybe the game is sensitive to DPI scaling, but disabling it had no effect; I tried tinkering with the compositor settings and disabling it, nothing, I tried different DEs, nope, then I tried using the Wine virtual desktop to emulate an 800x600 screen and... I got something:

It's still broken, but at least we can see the buttons and use it. What's weird is that only the main screen seems to be broken like that, the other screens are fine.

I was really puzzled as to what could cause such a weird behavior, but then I noticed something...

Screenshot showing window metrics

The virtual screen is 800px wide, the window is 486px wide, it's is shifted by exactly 157px from the left, and the content inside the window is also shifted from the left by 157px.

157px is an interesting number because it happens to be (800px-486px)/2, that's exactly how many pixels we would need to move the window from the left if we wanted to center it, and we can see a similar gap vertically.

If we try a different resolution for the virtual desktop like 1024x768 we can see the same thing happening, just more shifted to the right and to the bottom.

Screenshot at 1024x768 in the virutal desktop

So basically when I run the game normally on my display, the contents of window are shifted outside the visible area and I only see the black background under it.

In other words, the game is shitting itself trying to CENTER THE FUCKING WINDOW!

But why?

Before we dive deeper into it, I need to explain why I want to fix the launcher. The game and the launcher are a single executable and they're quite integrated. When Game.exe is launched without arguments, the launcher is displayed (which is a regular Win32 application), when the user starts a new game or loads an existing one, Game.exe is relaunched with the appropriate command line arguments and it starts the game instead of the launcher. While it would be possible to recreate the launcher from scratch (and maybe add a few more settings to it), it would take a lot of time and it wouldn't look like the original, which is something I want to preserve.

To figure out what's going wrong, we need to take a closer look at our Game.exe using both WINEDEBUG=+relay and our old friend Ghidra.

With the Wine relay enabled I was able to see all the Windows system calls emitted by the application, and discover that the launcher calls a function called SetWindowPos with several parameters right before the launcher becomes visible, which was a good starting point.

With Ghidra, I searched all the occurrences of SetWindowPos in the exe and there were a few, but one in particular looked promising because it was called right after an internal function called OnInitDialog.

Disassembly of the function showing the call to SetWindowPos after OnInitDialog

The pseudo-C code on the right makes it easy to understand what's going on:

  • Two variables called cx and cy are initialized with the width and height of the window (486 and 566 respectively)
  • Two calls to GetSystemMetrics are used to obtain the width (iVar3) and height (iVar2) of the display
  • The offsets needed to center the window are calculated:
    • From the top: iVar2=(height of display-cy)/ 2
    • From the left: iVar3=(width of display-cx)/2
  • A single call to SetWindowPos is used to move the window to coordinates iVar2,iVar3, resize it to cx,cy and make it visible
  • SetWindowPos is called again on the same window but the arguments passed make no sense to me and it has no visible effect (maybe a workaround for older versions of Windows?)
  • More internal functions are called...

From the trace provided by Wine I can see that the dimensions passed to the first SetWindowPos are correct but the fact that it passes 0xFFFFFFFF as the previous window handle instead of the usual NULL caught my attention. I tried replacing that handle with NULL but it didn't do anything (I guess it simply ignores invalid handles) so I moved my attention to the second SetWindowPos call, the weird one.

I simply replaced the second call with some NOPs and launched the game again. As I suspected, nothing changed because it didn't seem to do anything in the first place.

Then I moved back to the first call to SetWindowPos and replaced that with NOPs as well, at worst it just won't be centered, right? For some odd reason, this not only fixed the problem, but the window was still centered!

Fixed launcher

To clean up the code, I decided to simply skip over the entire attempt to center the window with a JMP from after the call to OnInitDialog to after the second call to SetWindowPos.

Disassembly of the function showing the skipped code

The launcher now works properly and I can get into the game.

Two big questions remain unanswered:

  • Why is the window still centered?
  • Why were some of the contents of the window being moved instead of the window itself?

The first question is easy to answer: by the time the code above gets called, the window has already been centered and made visible by code inside WINDOW.DLL, so the additional buggy code was unnecessary.

The second question, I have absolutely no clue so I tried replicating the issue. Using Visual Studio I quickly made an application that calls SetWindowPos to center the window in different ways: before making the window visible, after making it visible, while it's becoming visible, with decorations, without decorations, with invalid window handles, from multiple threads, everything I could think of, but I wasn't able to cause this glitch in either Windows or Wine so eventually I gave up. Now, I'll admit, I'm not an expert in Win32 development so maybe my little fuzzer didn't abuse these functions enough, if you know a better way to test it please let me know.

A quick look inside the Wine source code for SetWindowPos reveals that its implementation is inside dlls/win32u/window.c in a function called _apply_windowpos, but it's pretty deep into Wine and I can't really tell what's going on. All I can see are some checks, error handling, handling of a surface, and communication with the Wine server process, nothing looks obviously broken. The 1994 copyright notice is not particularly encouraging though.

At this point, I was bored, out of ideas and the problem was fixed anyway so I decided to play the game for a bit to see if it works properly.

Guess who's back

The old transparency issue is back.

Transparency issues on the trees

The issue is caused by the texture precaching implemented in the original DX7 renderer, which can be disabled by editing an ini file, but instead I decided to modify D3DDrv.dll itself.

Using Ghidra, I found the default value for the UsePrecache property and changed it from 1 to 0. This way, no config files need to be edited and applying the fix will be as simple as copying a couple of files.

Code for u_UsePrecache

Another solution could be using a modern DX11 renderer for UE1 games, which would also allow for higher resolutions and additional graphical effects, but we want none of that, we want to experience the game as it was intended by the developer.

Fixed transparencies

Nailed it.


You can get the fixed files from here: download archive

If you're using Wine you probably know what to do with these files already, but just in case you don't:

  • Navigate to Wine's virtual C drive, the default path is ~/.wine/drive_C
  • Enter Program Files (x86), EA Games, Harry Potter and the Chamber of Secrets, system
  • Extract the two files from the downloaded archive into this folder, overwriting the existing ones

And here's the final result:

The best part? This fix works on Windows too, so I'll soon update that fix too.

Share this article