Sprite rendering benchmark¶
The renderer draws sprites in batches: it gathers the frame's sprites, sorts them by layer and texture, and sends them in a handful of draw calls. The graphics study chose this design to keep the frame time low when the screen is full of missiles, enemies and effects. This page measures it: the same scene is drawn batched and naively (one upload, bind and draw call per sprite, as a renderer without batching would), with more and more sprites, on the team's machines.
Method¶
The scene is aegis2d_example_stress (see Getting started): N
sprites of 4 textures, 8 to 24 pixels wide, bounce around a 1280 x 720 screen. For each
number of sprites and each mode, tools/benchmark.py runs it for 300 frames with vsync off
and keeps every frame's times, the first 10 frames left out:
- CPU time: moving the sprites, drawing them and ending the frame, buffer swap included. It is what limits the frame rate here, and what batching saves: every draw call is driver work on the CPU.
- GPU time: the drawing measured by OpenGL timer queries, uploads included.
- FPS: 1000 divided by the mean CPU time.
The build is client-release (optimized), the window 1280 x 720, nothing else running.
Measure a machine¶
cmake --preset client-release && cmake --build build/client-release
python3 tools/benchmark.py run --exe build/client-release/examples/aegis2d_example_stress \
--name "Windows 11, RTX 3060"
python3 tools/benchmark.py report
run writes docs/benchmark/<machine>.csv (one line per run); report rewrites the
results below and the charts from every file of docs/benchmark/. On Windows, give the
path of aegis2d_example_stress.exe and use py for python3. Commit the CSV file, the
chart and this page together.
Results¶
Linux container, software OpenGL (no GPU)¶
Ubuntu 24.04, 2 vCPU Xeon 2.1 GHz, Xvfb; OpenGL renderer: llvmpipe (LLVM 20.1.2, 256 bits) (Mesa); 290 frames per run after 10 warm-up frames, vsync off.
| Sprites | Draw calls, batched / naive | Median CPU, batched (ms) | Median CPU, naive (ms) | Naive / batched | p95 CPU, batched / naive (ms) | Median GPU, batched / naive (ms) | FPS, batched / naive |
|---|---|---|---|---|---|---|---|
| 1000 | 4 / 1000 | 3.264 | 4.002 | x1.23 | 4.260 / 4.586 | 0.008 / 0.008 | 293.2 / 246.0 |
| 5000 | 4 / 5000 | 14.398 | 16.994 | x1.18 | 18.432 / 20.908 | 0.020 / 0.019 | 68.0 / 57.0 |
| 10000 | 4 / 10000 | 27.691 | 33.597 | x1.21 | 31.978 / 40.121 | 0.033 / 0.035 | 35.7 / 29.1 |
| 25000 | 4 / 25000 | 72.340 | 87.523 | x1.21 | 85.555 / 102.848 | 0.102 / 0.105 | 13.6 / 11.2 |
| 50000 | 4 / 50000 | 148.673 | 176.439 | x1.19 | 170.885 / 208.147 | 0.219 / 0.216 | 6.6 / 5.6 |
MacBook M3¶
Darwin 27.0.0; OpenGL renderer: Apple M3 (Apple); 290 frames per run after 10 warm-up frames, vsync off.
| Sprites | Draw calls, batched / naive | Median CPU, batched (ms) | Median CPU, naive (ms) | Naive / batched | p95 CPU, batched / naive (ms) | Median GPU, batched / naive (ms) | FPS, batched / naive |
|---|---|---|---|---|---|---|---|
| 1000 | 4 / 1000 | 1.747 | 200.287 | x114.65 | 3.475 / 212.087 | 0.468 / 199.302 | 489.3 / 4.9 |
| 5000 | 4 / 5000 | 2.881 | 1005.089 | x348.87 | 6.330 / 1006.679 | 1.289 / 1001.760 | 312.4 / 1.0 |
| 10000 | 4 / 10000 | 2.575 | 1990.664 | x773.07 | 6.071 / 1990.664 | 1.054 / 1989.052 | 307.5 / 0.5 |
| 25000 | 4 / 25000 | 9.570 | 5012.205 | x523.74 | 12.684 / 5012.205 | 5.143 / 5011.218 | 112.2 / 0.2 |
| 50000 | 4 / 50000 | 11.312 | 9922.341 | x877.15 | 17.467 / 9918.638 | 5.554 / 10091.869 | 79.1 / 0.1 |
Reading the results¶
- A software renderer hides the difference. Without a GPU, Mesa's llvmpipe draws on the CPU: most of the frame goes to filling pixels, the same in both modes, and a draw call costs little more than a function call, so batching saves only part of the frame. Its GPU times are not meaningful either, since the drawing happens when the frame is swapped. Such a machine shows the method; the machines with a GPU give the numbers.
- With a GPU driver, each draw call costs the CPU: validation, state changes and a buffer upload per sprite. The naive mode's time grows with the number of sprites much faster than the batched mode's, whose number of draw calls stays the same (one per layer and texture).
- 60 FPS is the dashed line of the charts: a frame must take less than 16.7 ms.