From 40 to 140 FPS: finding the cause, not the symptom
One day a game started running at 40 FPS. It had not been like that before, the hardware had not changed, and a driver update should not have made that much difference.
My first move was the one anybody would make: I lowered the resolution. Went from 1440p to 1080p, turned on an upscaler — and got 200 FPS. Problem "solved".
Except it was not a solution. It was muffling the symptom. I bought the monitor for 1440p, not to run it at 1080p. And more importantly, I did not know why it had happened. Whatever you do not understand comes back later.
This article is about exactly that: the difference between closing a fault and finding its cause. The example is from a game, but the method applies to any production problem.
Why "it works now" is not an answer
The fact that lowering the resolution helped gave me one piece of information: the bottleneck was in pixel work. Useful, but not enough, because the question stays open — why did the amount of pixel work suddenly go up? The resolution had not changed.
Software works the same way. "I added a retry, it does not fail anymore" is not a fix. It is a way of making the error invisible to the counter. The cause is still there and will return in another shape.
So I stepped back and reframed the question: who is asking for that many pixels to be drawn?
First measurement: where the bottleneck is
I looked at the GPU first. The numbers came out like this:
power temp load video memory FPS
1440p 58W 84C 99% 3753 MB 40
1080p+FSR 34W 78C 64% 3409 MB 200The most important column is video memory. It barely moved: 3753 versus 3409 MB. So memory was not the bottleneck. Load, meanwhile, dropped from 99% to 64%.
Together those two numbers give a clear answer: the GPU was not starved for memory, it was pinned on compute. It really was drawing far too many pixels.
This is the single most useful step in debugging: measure before you guess. My "it must be out of memory" hypothesis was killed by a one-minute measurement and pointed me in a completely different direction.
Second measurement: how many pixels is it actually drawing
If the GPU is drawing too many pixels, you need to know exactly how many. I asked the system for the window's real size — and the answer was unexpected.
My monitor is 2560x1440. The system was describing it like this:
$ xrandr --listmonitors
0: +*HDMI-A-1 4480/597x2520/336+2560+0
Screen 0: current 7040 x 25204480x2520. That is 11.3 megapixels — three times what 1440p is. In fullscreen the game was drawing exactly that many pixels, and the system was then squeezing the result down to 1440p. All the extra work was thrown away.
That was the moment everything fell into place. 40 FPS was not a wrong number. For three times the work, it was precisely the right number.
The cause: one global setting
The rest was easy. The laptop has two screens: an internal high-density panel with a 1.75 scale factor, and an external monitor that needs no scaling at all.
The compositor uses one shared scale for legacy X11 applications, and it picks the largest one — 1.75. That value spreads to the monitor that does not need it. As a result, the external monitor appears to legacy applications not as 2560x1440 but as 4480x2520.
And the game was launching in exactly that legacy mode — its own startup script was configured that way.
So the cause was this: the internal screen's scale factor was forcing a game on the external monitor to do three times the work.
The fix, and why I picked the second option
There were two paths.
The first was to run the game fully in the modern mode, bypassing the legacy layer. FPS recovered, but a side effect showed up: with two monitors the game always opened on the wrong screen, and its own in-game monitor selection stopped working.
The second was to drop the global scale to 1, letting the system upscale legacy applications itself. After a logout and login: 40 FPS to 140.
I picked the second. It has a price — legacy applications look slightly blurry on the laptop panel. But that price is worth paying, because the fix covers every legacy application at once rather than one game.
That is part of debugging too: you pick not the prettiest fix, but the one with the fewest side effects.
The next bottleneck
After the fix I measured again — and saw the second limit: the CPU sitting at 94-97 degrees and throttling for 524 milliseconds out of every three seconds. The constraint is no longer the GPU, it is cooling.
That is normal. Open one bottleneck and the next one becomes visible. What matters is confirming each with a measurement rather than a guess.
The method
What is worth taking from this story is not the settings but the order of operations:
- A workaround that works is information, not a solution. It only tells you where the bottleneck is.
- Measure before you theorise. My memory hypothesis died in one minute.
- Ask what the system thinks, not what you know. My monitor is 1440p, but the system believed it was 4480x2520 — the entire answer lived in that gap.
- Once you have the cause, choose the fix by its side effects.
- Measure again after fixing. There is always a next bottleneck.
Software behaves identically. A slow API, creeping memory, an error that shows up every other time — the same question applies to all of them: am I closing a symptom, or have I found the cause?
If you cannot answer that, the problem is still there.