Skip to content

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

Linux container, software OpenGL (no GPU): median CPU time per frame against the number of sprites, batched and naive.

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

MacBook M3: median CPU time per frame against the number of sprites, batched and naive.

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.