Game Optimization Methodology, or How 3 Days of Building a Test Harness Gave Me More FPS Than a Month of Writing Code
Description and use
After rebuilding the test harness with proper measurement methodology (metadata, dry runs, physics-tick-count-controlled duration, multiple scenarios, raw metrics, better statistics), the developer successfully identified and addressed performance spikes caused by long physics-server work within rendered frames. The custom physics server using a rebuilt-every-frame hash grid, SoA position/size storage, and simplified solver, combined with texture atlas merging and font baking, produced measurable FPS improvements. The second attempt took five days total, with three days spent building the test harness.
Problem
A mobile game developer needed to optimize a Godot-based game (Brotato) running on weak ARM handheld hardware (Anbernic RG34XXSP) where the game dropped to 5–10 FPS from wave 20 onward, with no reliable way to measure whether optimization changes actually improved performance.
Outcome
After rebuilding the test harness with proper measurement methodology (metadata, dry runs, physics-tick-count-controlled duration, multiple scenarios, raw metrics, better statistics), the developer successfully identified and addressed performance spikes caused by long physics-server work within rendered frames. The custom physics server using a rebuilt-every-frame hash grid, SoA position/size storage, and simplified solver, combined with texture atlas merging and font baking, produced measurable FPS improvements. The second attempt took five days total, with three days spent building the test harness.
Workflow
- Extract the Godot project files from the game distribution
- Build a simple test harness that spawns a potato on wave 100 with a fixed seed, records frame time, draw calls, and collision pair counts per frame, and writes average values to a file at the end
- Identify physics server collision processing as the main bottleneck (huge number of collision pairs per tick) and draw calls/GPU state changes as secondary bottlenecks
- Replace Godot's physics server with a lightweight implementation using a rebuilt-every-frame hash grid, SoA position/size storage, and a simplified solver
- Merge all textures into a single atlas and bake fonts into bitmap format to reduce draw calls and enable batching
- Run tests on the RG34XXSP console and debugger runs on PC to verify improvements
- Discover the console died unexpectedly, preventing final testing on target hardware
- Try optimizations on Android version (Samsung A51) and find no improvement beyond measurement noise due to flawed measurement methodology
- Dig deeper into theory and identify measurement mistakes: no run metadata, no dry runs, using elapsed time instead of physics-tick count to control test duration, only one test scenario, not saving raw run metrics, using arithmetic mean
- Rebuild the test harness with proper run metadata, dry runs, physics-tick-count-controlled duration, multiple test scenarios, raw run metrics, and better statistical representations (median, P95, P99, worst value)
- Implement data processing in an external script that estimates variance and builds graphs making anomalies visible
- Add a compare mode that checks metadata validity of two recordings before calculating relative metric changes
- Connect Samsung A51 to PC via ADB, execute all tests automatically on the phone, and pull resulting data back to PC for analysis
- Implement custom physics server and batching optimizations, then verify results through the improved harness
Evidence
On the RG34XXSP's rather weak hardware, the game dropped to 5–10 FPS from around wave 20 onward.
The main bottleneck was collision processing inside Godot's physics server. This was visible from the huge number of collision pairs per tick, and I was able to confirm it by running the game at half the original physics TPS
The secondary bottleneck was the number of draw calls and GPU state changes. Textures were stored in separate files, forcing resource switches between draws and breaking batching
- Record date: 2026-10-20T18:56:43.134Z