From 1388c818f4390cd718f2f4b65c86412bc1129a7f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 9 Aug 2026 05:10:52 +0000 Subject: [PATCH] docs: record sprite draw performance findings Co-authored-by: codegirl007 --- ...erministic-sprite-performance-benchmark.md | 79 ++++++++++++++++++ .../issues/profile-optimized-sprite-builds.md | 76 +++++++++++++++++ .../issues/simplify-draw-sprite-clip-math.md | 81 +++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 .github/issues/add-deterministic-sprite-performance-benchmark.md create mode 100644 .github/issues/profile-optimized-sprite-builds.md create mode 100644 .github/issues/simplify-draw-sprite-clip-math.md diff --git a/.github/issues/add-deterministic-sprite-performance-benchmark.md b/.github/issues/add-deterministic-sprite-performance-benchmark.md new file mode 100644 index 0000000..3365f1b --- /dev/null +++ b/.github/issues/add-deterministic-sprite-performance-benchmark.md @@ -0,0 +1,79 @@ +# Add deterministic CPU and frame benchmarks for sprite rendering + +## Summary + +The current `make flame` workflow is valuable for finding call stacks, but it +does not provide a reproducible performance metric: + +- `examples/crowd` draws only 24 sprites. +- `MAX_SPRITES` limits the renderer to 128 sprites. +- Recording starts an interactive application and asks the user to play for + 10–20 seconds before quitting. +- The two checked-in reports contain only 499 and 469 samples. +- CPU queue construction and GPU submission are combined in one profile. +- The profiler currently builds unoptimized code. + +These limitations make it difficult to tell whether a change made +`draw_sprite` faster, changed driver behavior, or merely changed sampling noise. + +## Evidence + +A temporary deterministic benchmark exposed two very different results: + +1. CPU-only `draw_sprite`, two million calls and seven trials: + - `-debug`: median 192.560 ns/draw + - `-debug -o:speed`: median 19.154 ns/draw +2. Full 128-sprite frames through SDL GPU on Lavapipe, 1,000 measured frames + and five trials: + - `-debug`: median 2.486 ms/frame + - `-debug -o:speed`: median 2.447 ms/frame + +At the current cap, optimized CPU queue construction is approximately 3.2 +microseconds for 128 sprites. The full software-rendered frame is around 2.45 +milliseconds, so optimizing `draw_sprite` cannot materially improve that +specific end-to-end workload. A hardware GPU or a larger future sprite limit +may have a different balance. + +This split also explains why percentages from the current unoptimized +flamegraphs overstate small helper functions. + +## Proposed change + +Add a non-interactive benchmark target with two explicitly separate workloads. + +### CPU queue benchmark + +- Construct `App`, `Character_Data`, and `Sprite` with real baked metadata. +- Use safe fake non-null GPU handles; `draw_sprite` only checks/stores these. +- Preallocate the draw list. +- Clear the queue whenever it reaches `MAX_SPRITES`. +- Vary sprite position between calls so the compiler cannot hoist the work. +- Warm up before timing. +- Run at least one million calls and report nanoseconds per draw. +- Build with `-o:speed` by default. + +### Full-frame benchmark + +- Use a real SDL GPU device and baked texture. +- Warm up before timing. +- Run a fixed number of frames without interactive input. +- Report milliseconds per frame and sprites per second. +- Record GPU backend, present mode, compiler version, compiler flags, and sprite + count. + +The CPU benchmark should be available without a display or GPU. The full-frame +benchmark may remain opt-in where a suitable GPU backend is unavailable. + +Do not add a strict CI regression threshold initially; hosted runner variance +will make a single threshold flaky. CI can still compile the benchmark and +verify that it completes. + +## Acceptance criteria + +- A Makefile target runs the optimized CPU benchmark non-interactively. +- Results include compiler flags, iteration count, median, and per-trial values. +- CPU queue time is reported separately from complete frame time. +- Sprite positions or frames vary during the measured loop. +- The draw list never silently exceeds `MAX_SPRITES`. +- The benchmark has documented commands for repeatable local comparison. +- `make check` and `make test` continue to pass. diff --git a/.github/issues/profile-optimized-sprite-builds.md b/.github/issues/profile-optimized-sprite-builds.md new file mode 100644 index 0000000..cf0349f --- /dev/null +++ b/.github/issues/profile-optimized-sprite-builds.md @@ -0,0 +1,76 @@ +# Profile optimized sprite builds instead of unoptimized debug code + +## Summary + +`make flame-build` currently compiles the selected example with `-debug` but +without an optimization mode: + +```make +odin build examples/$(FLAME_EXAMPLE) -collection:pkg=. -out:$(FLAME_BIN) -debug +``` + +This makes the flamegraph useful for debugging but misleading for performance +decisions. The current profiles largely describe code that will disappear or +be inlined in an optimized build. + +## Evidence + +The two checked-in `crowd` profiles were captured from this unoptimized binary. +They report: + +- `engine::draw_sprite`: 9.56% and 12.22% self time +- `engine::to_clip`: 5.33% and 5.51% self time +- `engine::sprite_feet_quad`: 3.98% and 3.30% self time +- Additional time in string hashing/map lookup, bounds checks, and dynamic + array append helpers + +A CPU-only benchmark using the real `draw_sprite`, real baked toad metadata, +preallocated draw list, changing sprite positions, and two million draws per +trial produced: + +| Build | Median time per draw | Trials | +| --- | ---: | ---: | +| `-debug` (current Makefile behavior) | 192.560 ns | 7 | +| `-debug -o:speed` | 19.154 ns | 7 | + +The optimized build is about **10.1x faster** without an engine code change. + +An end-to-end 128-sprite benchmark on SDL 3.4.12 with Lavapipe showed only a +small full-frame difference (median 2.486 ms debug versus 2.447 ms optimized) +because software GPU/driver work dominated. This does not invalidate the CPU +result; it shows why CPU queue time and full-frame time must be reported +separately. + +Environment: + +- Odin `dev-2026-05-nightly:ea5175d` (the version pinned by CI) +- SDL 3.4.12 +- Linux x86-64 + +## Proposed change + +Compile profiling binaries with optimization while retaining symbols: + +```make +FLAME_ODIN_FLAGS ?= -debug -o:speed + +flame-build: + odin build examples/$(FLAME_EXAMPLE) -collection:pkg=. \ + -out:$(FLAME_BIN) $(FLAME_ODIN_FLAGS) +``` + +Keeping the flags configurable allows an explicitly unoptimized diagnostic run +without making it the performance default. + +Consider applying an explicit optimization mode to performance-oriented example +runs as well. Plain `odin run` currently uses Odin's unoptimized default. + +## Acceptance criteria + +- `make flame-build` produces an optimized binary with debug symbols. +- `FLAME_ODIN_FLAGS` can override the default for diagnostic builds. +- `make check` and `make test` continue to pass. +- A new `crowd` profile records the exact compiler flags in its report or + accompanying documentation. +- Performance conclusions distinguish CPU `draw_sprite` cost from complete + frame/GPU submission cost. diff --git a/.github/issues/simplify-draw-sprite-clip-math.md b/.github/issues/simplify-draw-sprite-clip-math.md new file mode 100644 index 0000000..99f68e8 --- /dev/null +++ b/.github/issues/simplify-draw-sprite-clip-math.md @@ -0,0 +1,81 @@ +# Reduce repeated clip-space work in `draw_sprite` + +## Summary + +`draw_sprite` calls `to_clip` four times for an axis-aligned quad: + +```odin +p0 := to_clip(x0_px, y0_px, sw, sh) +p1 := to_clip(x1_px, y0_px, sw, sh) +p2 := to_clip(x1_px, y1_px, sw, sh) +p3 := to_clip(x0_px, y1_px, sw, sh) +``` + +This repeats the same divisions and converts duplicate x/y coordinates. An +axis-aligned sprite has only two unique x values and two unique y values. + +This is a measurable optimization, but it is low priority at the current +128-sprite limit because the absolute saving is small. + +## Evidence + +The checked-in unoptimized profiles report `engine::to_clip` at 5.33% and 5.51% +self time. Those percentages are inflated by the unoptimized profiling build, +so the change was also measured with `-debug -o:speed`. + +A temporary benchmark used the real baked toad metadata, changed sprite +position on every iteration, preallocated the queue, and performed two million +draws per mode over seven trials: + +| Mode | Median time per draw | +| --- | ---: | +| Current `draw_sprite` | 25.128 ns | +| Precomputed clip scale and reused coordinates | 21.531 ns | +| Same math plus cached `Frame_Def` | 21.778 ns | + +Simplifying the math improved isolated draw time by approximately **14.3%**. +Caching the resolved frame did not provide an additional benefit and should not +be included without new evidence. + +At `MAX_SPRITES == 128`, the measured math saving is only about 0.46 +microseconds per completely full frame. GPU/driver work dominated the +end-to-end benchmark, so this should follow the profiling and deterministic +benchmark improvements. + +Environment: + +- Odin `dev-2026-05-nightly:ea5175d` +- Optimized with `-debug -o:speed` +- Linux x86-64 + +## Proposed change + +Compute clip scaling once and construct corners from the unique coordinates: + +```odin +sx := 2.0 / f32(app.swapchain_w) +sy := 2.0 / f32(app.swapchain_h) + +left := x0_px * sx - 1 +right := x1_px * sx - 1 +top := 1 - y0_px * sy +bottom := 1 - y1_px * sy + +p0 := Vec2{left, top} +p1 := Vec2{right, top} +p2 := Vec2{right, bottom} +p3 := Vec2{left, bottom} +``` + +Keep `to_clip` for general callers and its existing tests; this change only +specializes quad construction inside `draw_sprite`. + +## Acceptance criteria + +- Existing sprite geometry, camera, UV, and batching tests pass. +- Add or extend a test that compares all four generated corners against + `to_clip` for representative viewport and sprite coordinates. +- Flipped and unflipped sprites produce identical vertices to the current code. +- An optimized deterministic benchmark shows at least a 10% improvement in + isolated `draw_sprite` time under comparable conditions. +- Do not add a per-sprite frame cache as part of this issue.