Skip to content

[M2-PROF-01] CPU/GPU render timing (RENDER-005) - #96

Merged
offdev merged 2 commits into
masterfrom
feat/m2-prof-01-render-timing
Oct 12, 2026
Merged

offdev merged 2 commits into
masterfrom
feat/m2-prof-01-render-timing

Conversation

@offdev

@offdev offdev commented Oct 12, 2026

Copy link
Copy Markdown
Owner

[M2-PROF-01] CPU/GPU render timing (RENDER-005)

Implements the M2-PROF-01 step exactly per its Scope/Verify clauses in roadmap/M2-rendering-2.5d.md: CPU and GPU measured independently — per-pass CPU phase times (batch, sort, submit) from the render-thread timers, per-pass GPU time via a GL TIME_ELAPSED query object (opt-in, one frame late, honest "n/a" headless), stall detection for the CPU waiting on the GPU's implicit upload sync (rate-limited), and the profiler feed of the render values.

API

  • SpriteBatcher — SpriteBatchTiming (frameTiming(): batchMs = the frame window, beginFrame()/create up to build() start; sortMs = build() itself) + SpriteBatchTimingTotals (timingTotals(), successful builds only). Two steady_clock reads per frame (+ one at create), no allocation.
  • SpriteRenderer::Options — gpuTiming (off by default; ONE reused GL_TIME_ELAPSED query object created at create, a begin/end pair around the pass, one availability read per frame — no per-frame GL allocation, no CPU/GPU synchronization) + stallThresholdUs (documented default kSpriteRendererDefaultStallUs = 2000 us ≈ 12% of a 60 Hz frame; domain [1, 0xFFFFFFFF]; create validation, first failure wins, the options_invalid warn).
  • SpriteDrawStats — submitMs, uploadWallUs, uploadStalled, gpuTimeUs/gpuTimeValid (one frame late: frame N reports frame N−1's pass; gpuTimeValid false = "n/a" — first frame, GPU more than one frame behind, or an environment without working timing queries; never a fake number; a failed submit poisons only its own in-flight measurement). SpriteDrawTotals — the submitMs, uploadStallUs/uploadStallFrames, gpuTimeUs/gpuTimeFrames sums.
  • Stall detection — the pass's only implicit CPU↔GPU sync point is the one glBufferSubData into the reused instance buffer; its CPU wall time above the threshold → uploadStalled + one rate-limited Warn sprite_renderer/submit_stall (fields stall_us, upload_bytes). Observation, never an execution gate (the G-R2 precedent).
  • Profiler feed (M1-PROF-01, primitive-typed — laige-sim cannot depend on laige-render types, ARCH-001): addRenderBatchMs/addRenderSortMs/addRenderSubmitMs/addRenderGpu/addRenderStall/setRenderTextureMemory; the laige-profile render: text line, the JSON report's "render" object (gpu_us is null until the first completed measurement), and the summary line's render fields (render_gpu_us=n/a on the same rule). The engine's render-thread wiring lands with the engine integration step (the addDrawCalls feed precedent).
  • VRAM estimate: the pre-existing textureMemoryBytes() gauge now feeds the profiler.

Tests

  • render_timing_tests.cpp (new CTest entry render_timing = the step's Verify command; 8 tests / 5 suites): RenderTimingCreate (GL-free: the stallThresholdUs validation + first-failure-wins + the stopped state reads zero), RenderTimingCpu (GL: the CPU phase times, the EXACT totals accumulation, the empty frame records nothing, the profiler feed wiring), RenderTimingGpu (GL: off-by-default "n/a"; on: the one-frame-late semantics under glFinish-paced frames, exact totals), RenderTimingStall (GL: the 4096-sprite frame under a 1 us threshold stalls — one Warn per stalled frame with the pinned fields; the small frame under the default threshold does not), RenderTimingSlowShader (GL: the deliberately slow shader — a 16384-iteration dependent sin loop — with the GPU-side pass time EXCEEDING the CPU-side submit time on a hardware-GL runner; SKIPS on a software renderer, where the CPU is the GPU and the driver is synchronous).
  • profiler_tests.cpp — the render-timing counter-model tests (the exact accumulation, the "n/a" GPU contract, the disabled no-op preserving state, the greppable text/JSON report surfaces, the zero-alloc record path now covering the 6 render feeds).
  • GL suites carry the documented environment contract (GTEST_SKIP where no usable OpenGL 3.3) + the ASan detect_leaks=0 / TSan property entries; TIMEOUT 120.

Docs / surface

  • docs/api/sprite_renderer.md (new "Render timing" section + the API/Performance/traps updates), docs/api/sprite_batcher.md (the timing fields), docs/api/profiler.md (the counter table, summary line, text line, JSON object), both module READMEs, tools/README.md (the report schema line).
  • laige-api.json regenerated LAST (1815 → 1851 symbols, 48 headers). budgets.json unchanged (17 entries — the timing is O(1) bookkeeping; the composite 50k render-CPU budget is measured with M2-PERF-01).
  • Roadmap box checked + the Shipped note; Progress Board M2 29/33 (total 75/194) + the Change Log row.

Verification

  • Local 4-tree Linux gate (AGENTS.md §5): gcc / clang / asan / tsan, full ctest minus the GL-creating entries (GPU local-execution policy; the CI lanes run them unfiltered) — results in the Change Log row.
  • laige-include-lint + laige-determinism-lint green; api-real-tree / api-check-fresh in the gate.
  • CI: ctest -R render_timing on every P0 OS job (Linux llvmpipe + Windows desktop runner — the slow-shader GPU>CPU observation is defined for the hardware-GL desktop runner; macOS runners skip all GL, documented).

Size: beyond the ~250-line roadmap guidance (the documented contract spans renderer + batcher + profiler + five docs, per CORE-006/DOC-004) — noted in the Shipped entry.

…PU measurement)

Per-pass CPU phase times (batch, sort, submit) from the render-thread
timers; opt-in per-pass GPU time via one reused GL TIME_ELAPSED query
object (one frame late, honest 'n/a' headless — never a fake number);
stall detection for the CPU waiting on the GPU's implicit upload sync
(one rate-limited Warn per stalled frame); the M1-PROF-01 profiler
gains the primitive-typed render feeds (text/JSON/summary surfaces).

Tests: ctest -R render_timing (8 tests: the stallThresholdUs
validation + stopped state, the CPU phase times with exact totals,
the GPU timing's one-frame-late semantics, the stall detection, and
the slow-shader fixture with GPU time > CPU time on a hardware-GL
runner, SKIPPED on software renderers) + the profiler
counter-model render-timing tests.

laige-api.json 1815 -> 1851 symbols; budgets.json unchanged (17).
Local 4-tree Linux gate green (117/117, 117/117, 112/112, 112/112
minus the 10 GL-creating entries per the GPU policy); include-lint +
determinism-lint green. Roadmap box checked; Progress Board M2
29/33 (total 75/194).
The GL test suites passed SpriteRenderer::Options{} defaults to
makeScene; the default maxInstances/maxAtlases are 0, outside the
documented domain [1, ...], so SpriteRenderer::create failed
invalid_argument on every P0 CI runner (first CI run 38303505479:
the render_timing + unfiltered laige-render_tests entries aborted on
all three Linux trees). The tests now set the per-scene budgets
explicitly (16 instances / 2 atlases, 8192 for the stall scene).
@offdev
offdev merged commit 5aba6e3 into master Oct 12, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant