Parts
Part 1: Decoding the Gears of War 2 graphical settings.
Part 2: Writing the code to read and write the settings.
Part 4: Modding the Gears of War 2 executable to run in 60 FPS.
Introduction
In part 3 we managed to change the FOV in the Xbox 360 exclusive Gears of War 2, while running on original hardware. This was achieved using the coalesced-inifier tool we wrote in part 2. Now that we can change arbitrary hidden settings, we can manipulate gameplay mechanics, re-balance the multiplayer and write profanities on the credits screen. Good job.
While these mods can be fun and genuinely useful, I think we should go a little deeper down this path. We can change the graphics settings to make the game perform better, but how much better can it perform? Does the hardware actually limit us to 30 FPS or is it just an arbitrary target set by stressed-out game developers during crunch time?
Let’s assume the latter, how do we then proceed with testing the hypothesis? The game is built on Unreal Engine 3, which was used for a lot of games. Some of these are PC games which means there are probably entire forums dedicated to changing the FOV (and other mods) of said titles. Let’s start with the most obvious one: Gears of War 1. This was actually released on PC and therefore it has a PCGamingWiki entry that should be a good place to start.
So the idea is to leverage knowledge about UE3 mods on PC to attempt to unlock the framerate on our Xbox 360 title. If we manage to do this we can use the coalesced-inifier tool to reduce the graphical fidelity to, hopefully, get a close-to-locked 60 FPS.
Disabling the frame limit
There are probably thousands of settings we can set in the game and probably hundreds that impact graphical fidelity directly. Let’s start by looking at some of the more obvious settings the devs left in the compiled INI files:
[SystemSettings]
...
DynamicLights = True
DynamicShadows = True
MotionBlur = True
DepthOfField = True
Bloom = True
MaxShadowResolution = 800
ResX = 1280
ResY = 720
ScreenPercentage = 100.000000
...
TEXTUREGROUP_World = (MinLODSize=256,MaxLODSize=1024,LODBias=1,MinMagFilter=aniso,MipFilter=point)
TEXTUREGROUP_WorldNormalMap = (MinLODSize=256,MaxLODSize=1024,LODBias=1,MinMagFilter=aniso,MipFilter=point)
These are, for the most part, self explanatory. We can actually disable the rather crude depth of field effects and motion blur and the bloom. This already makes the game more enjoyable because DoF and bloom did not age well. It also looks like we can change the render resolution, which will probably make a huge impact on the performance but make the game render with a lot of jaggies.
If we go looking around on the internet we find bits and pieces of information about the settings. A post on the forums for the game TERA describes quite a few of the settings in detail. The PCGamingWiki has an Unreal Engine 3 section, which includes instructions on how to disable the frame limiter. The UE3 frame limiter is controlled by a setting called bSmoothFramerate
, which sets the target FPS and, I assume, it smooths out the framerate if it is too erratic. There’s a min and a max FPS settings, this is presumably used to establish when the frame smoothing needs to run.
According to PCGamingWiki we have two options to go above the normal limit:
- Set
bSmoothFramerate = true
andMaxSmoothedFrameRate
to the desired FPS - Set
bSmoothFramerate = false
and ignore the rest
If we take a look at the default settings we see:
[Engine.GameEngine]
bSmoothFrameRate = TRUE
MinSmoothedFrameRate = 22
MaxSmoothedFrameRate = 62
MaxDeltaTime = 0.05
PendingLevelPlayerControllerClassName = GearGame.GearPC_PendingLevel
The game is running with the frame limit set to 62 FPS (the default in UE3), which means that the frame limiter does not prevent the game to go above 30. This leads me to conclude that something else is the culprit. Let’s look at the most obvious one, vertical sync:
[SystemSettings]
...
UseVsync = False
So vertical sync is disabled (at least the builtin UE3 one), which means the frame limit is most likely set by some graphics pipeline setting somewhere.
Down the binary rabbit hole
Do you know the feeling when you get a great idea you are sure is original, and then do a web search and notice that it’s been a thing for like half a decade? This is one of those cases. The Xbox 360 emulator fork Xenia has a related repository with game patches, some of which include a 60 FPS patch for Gears of War 2 (among other hacks). The TOML file describes a memory address and a value to set, for the FPS unlock patch it’s:
[[patch]]
name = "Unlock FPS"
desc = "See note about framerate patches in the README."
author = "illusion, boma"
is_enabled = false
[[patch.be32]]
address = 0x824a2e94
value = 0x60000000
[[patch.be8]]
address = 0x8298cfb3
value = 0x01
So at address 0x824a2e94
we need to write 0x60000000
and on address 0x8298cfb3
we need to write a 0x01
. The only issue is that the latter address equals 2191052723
, which is around 2 billion or 2 gigabytes. This puts the offset quite a lot outside of both the executable size and the RAM (512 MB) of the Xbox 360. This means we have some address translation going on and we need to load the executable and poke around.
Looking around the internet I found an XEX loader for Ghidra and fired it up. The analysis takes a couple of minutes but loads what looks like code in what I assume looks like PowerPC assembler.
Patch 1
Our first patch is 4 bytes at the address 0x824a2e94
:

It seems like we are modifying the return value. My C is quite rusty, but from the looks of it it seems that this function returns a pointer to a struct field at the offset 0x504
from the input parameter, which I assume is a pointer. Applying the patch turns the lfs f1,0x504(r3)
into a ori r0,r0,0x0
. The latter does r0 = r0 | 0
, which is always the value of r0
so this is a way to write a no-op in PowerPC. The effect of it is that we now return 0:

I have no idea what this is, but my guess is that the caller does a null check on the result of this function, and returning 0
in all cases forces that behavior.
Patch 2
Jumping to the last address 0x8298cfb3
shows us the following:

Changing the 4th byte to 0x01
sets uVar5 = 1
and looks like this:

I do not know exactly what this code does, but during my research on how one would implement FPS limiting in DirectX on the PC, I fell across the API docs for Direct3D 9. Whenever you create a Direct3D device you pass it these parameters, and if you pass it D3DPRESENT_INTERVAL_TWO
it will, quote:
The driver will wait for the vertical retrace period.
Present operations will not be affected more frequently than every second screen refresh.
Translated to human-speak this means that the stuff is rendered with VSYNC, but a frame is only sent to the display every second screen refresh. This sounds very much like the behavior we observe. The flip from 0x02
to a 0x01
here is probably a flip from D3DPRESENT_INTERVAL_TWO
to D3DPRESENT_INTERVAL_ONE
. Many hours were spent attempting to map this to a Direct3D call of some sort, but my Ghidra-fu is simply not good enough (yet).
Testing the patched binary
We could test this in an emulator but the title of this post includes the words ‘original hardware’, which means: grab your whisky (and your capture card). Exporting the file from Ghidra yields an XEX that my Xbox does not want to run. This means we need to apply the patches by raw-dogging them in a hex editor. But, XEX files are compressed and encrypted, which means we need to run them through XexToolGUI. With your whisky in hand and XexToolGUI running (runs fine in Wine), remove the encryption and the compression. You should now have a file of roughly 18 MB. You will have to first remove the encryption, load the new file and then remove the compression.
Now go back to Ghidra to the two patch locations and search copy a large enough byte pattern close to the patch location, and then search for that in your hex editor. In Okteta on KDE:

Conclusion
The journey has been long, but the result is very, very cool. The world can now enjoy Gears of War 2 in 60 FPS (with a wider FOV) on an original Xbox 360, which is, how do I say it? Awesome:
Thanks for reading.
/J