Patching the retail version of Half-Life 2 to get it running under Wine
For one reason or another, I recently found myself wanting to play the original retail 2004 build of Half-Life 2. Given that I now use Linux full-time, I have to do so under Wine, using Lutris to launch it and DXVK for the Direct3D 9 implementation. I quickly ran into some issues though.
The first I encountered is that it would freeze and try and consume all of my system memory. Ouch. This is because it allocates textures endlessly until it canât any more, in a poor attempt to determine how much video memory is avaiable. On a modern system with 12 GB of video memory and 48 GB of system memory, it ends poorly⦠Fortunately this one was trivial to fix: just add a `dxvk.conf` next to `hl2.exe` containing:
[hl2.exe] d3d9.maxAvailableMemory = 1024
Then I hit another issue: it thought I had less than 128 MB of system memory.
Specifically, it thought I had -1 bytes of system memory. Those of us (un)fortunate enough to know enough about Win32 can guess whatâs happening: itâs using `GlobalMemoryStatus`. This function returns the amount of memory the system has in a 32-bit integer, and it handles amounts of memory above 4GB very poorly:
> On computers with more than 4 GB of memory, the GlobalMemoryStatus function can return incorrect information, reporting a value of â1 to indicate an overflow.
On modern Windows, itâs possible to set a per-application compatibility level, which will make Windows will try and behave like a previous version of the OS. Setting it to Windows XP compatibility fixes this issue, and it will cause `GlobalMemoryStatus` to report a valid amount of memory. Unfortunately, Wine seemingly has no such compatibility setting for this function, at least that I could find. So, I decided to do the only reasonable thing: patch the binary to remove this check.
First, we need to find the module where the check is located. The Source engine is highly modular, and has a load of DLLs:
There are some Linux .so files there, but theyâre for the dedicated server. Client support for Linux was 9 years away yet.
Fortunately, finding the right one was trivial.
bin $ grep -rn 128MB dxsupport.cfg:2351: "name" "Matrox Parhelia 128MB" grep: engine.dll: binary file matches
Itâs in `engine.dll`. Time to crack it open using Ghidra. `engine.dll` is the largest module in the engine, so it took a little while to analyse.
Ghidra has a handy âDefined Stringsâ window that lets us view all of the strings in the module, and search through them:
From there, we can jump to where the string is referenced.
And we found the memory check! Itâs exactly as guessed: itâs using `GlobalMemoryStatus` which is returning an invalid value.
The check essentially compiled to a `JGE` instruction at memory location `0x20162057`: if the memory is above 128 MB, it will skip over raising an error. We can easily get rid of the check by replacing it with a `JMP`, so it always skips over the error: right-click the instruction and select âPatch Instructionâ.
Export the DLL from File > Export, selecting the Raw Bytes format. Ghidra appends a `.bin` to the file extension, so copy it over your `engine.dll` manually.
This has removed the memory capacity check. However, the code uses the memory size to determine the size of an internal engine heap; this will get clamped to the range of 48 to 64 MB. Due to unsigned shenanigans, the -1 value actually gets clamped to the upper of that bound. You can override it with `-heapsize` if you wish (it takes a value in kB).
Now you can play the retail build of Half-Life 2 on Linux. Have fun.
A slight caveat is that exporting the DLL from Ghidra doesnât just change the instruction; it exports the whole binary from its datamodel anew. As such while itâs functionally identical the file has might have some differences. If you just want to fix this issue, using a hex editor to patch it is probably better: given an `engine.dll` with SHA256 `21cbf8e15551906eab2b27348d43876167e8b7e520cee6efc95a18b7a96ee051`, change the byte at offset `0x162054` from `7d` to `eb`.
This was my first time actually patching a binary to get it working. At least it was simple⦠this article is basically about changing a single byte.