Skip to content

[M2-SPRITE-04] Render observability + draw-call budget (G-R2) - #75

Merged
offdev merged 2 commits into
masterfrom
feat/m2-sprite-04-render-observability
Oct 4, 2026
Merged

offdev merged 2 commits into
masterfrom
feat/m2-sprite-04-render-observability

Conversation

@offdev

@offdev offdev commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

M2-SPRITE-04 · Render observability + draw-call budget (G-R2)

Roadmap: roadmap/M2-rendering-2.5d.md (M2-SPRITE-04) · PRD §9.3 G-R2 ("Batcher reports; per-pass cap configurable → warn + frame graph flag") · RENDER-001.

What

On top of the M2-SPRITE-02 counters (the M2-PROF-01 profiler feed):

  • Profiler fields — SpriteDrawStats + programChanges, uploadBytes (n × 52 B), renderTargetBytes (w × h × 4), drawCallCapExceeded; SpriteDrawTotals + the matching sums + capExceededFrames.
  • G-R2 per-pass draw-call cap — Options::maxDrawCalls (documented default kSpriteRendererDefaultDrawCalls = 64 = 2× the PRD §8.1 worst-case reference-scene budget of 30 draw calls; domain [1, kSpriteRendererMaxInstances]). A frame strictly above the cap: one rate-limited Warn draw_call_cap (fields capacity, draw_calls) + the frame flag (the frame-graph flag M2-PROF-01 will report) + the total — and is still drawn (observation, never an execution gate — the G-R5 precedent).
  • textureMemoryBytes() — the texture-memory VRAM estimate gauge: bound atlases' w × h × 4 sum, updated at bindAtlas (re-bind subtracts old + adds new), 0 in the stopped state.
  • Contract — a failed submit zeroes the frame counters (enforced on all three GL failure paths: upload, query creation, draw).
  • The atlas registry slot grew 8 B → 16 B (width/height bookkeeping; 64 KB at 4096 slots).

Tests

New tests/laige-render/render_counters_tests.cpp — CTest entry render_counters (the step's Verify command), 8 tests / 4 suites:

  • RenderCountersCreate (GL-free): maxDrawCalls validation first-failure-wins + one Warn; the default reaching the context check → GlUnavailable; the stopped state reads zero.
  • RenderCountersScene (GL): the roadmap's known small scene — 10 sprites, 2 atlases, 2 blends → 3 groups — with the per-frame counters pinned exactly (frame 1 + frame 2 incl. the cross-frame last-atlas/last-blend persistence), the since-construction totals exact, the empty frame counts nothing, the opt-in PRIMITIVES_GENERATED feed = 20.
  • RenderCountersCap (GL): the warn fires at the configured count with the pinned fields, the frame still drawn, the repeat + total tracked, no flag/warn at or below the cap.
  • RenderCountersMemory (GL): the VRAM gauge exact through binds + re-bind replacements.

Verification

Tree Result
build (g++ Debug) 111/111
build-clang 111/111
build-release 100/100
build-shared 111/111
build-asan 108/108
build-tsan 108/108

TSan note (first commit): the CI TSan lane reproduced a driver-internal teardown data race — both race accesses inside libgallium (the eglDestroyContext teardown destroys the driver's internal mutex/condvar while the llvmpipe worker thread is still inside it; no engine code between the pthread frames). Bisected: it is triggered by a query-enabled submit as the FIRST FBO operation of the frame; readback quiesce and clear-after-draw do not fix it. The fix is the frame-pipeline clear-before-draw pattern in RenderCountersScene.PrimitiveQueryFeed (the SpriteDrawSmoke.ThousandSpriteFrame precedent) — the clear touches no sprite-pass state (counters unchanged), and the teardown is then clean (5/5 on Mesa 26.2.3 local; CI TSan on Mesa 25.2.8 re-verified by this PR's run).

laige-api.json regenerated (1233 symbols / 38 headers, +12); api-real-tree/api-check-fresh, tools/laige-include-lint, and tools/laige-determinism-lint all green.

Compat / scope

  • Additive only — no existing symbol's signature or meaning changed. New public API: 4 SpriteDrawStats fields + 4 SpriteDrawTotals fields + Options::maxDrawCalls + kSpriteRendererDefaultDrawCalls + maxDrawCalls() + textureMemoryBytes() (12 symbols).
  • No budgets.json entry: the counters are O(1) bookkeeping; the composite 50k render-CPU budget is measured with M2-PERF-01.
  • No ENGINE-RULE-EXCEPTION added.
  • Docs updated in the same change: docs/api/sprite_renderer.md (new "Render observability + the draw-call cap" section), docs/README.md index, the module README, the roadmap box + progress board (M2 16/33, total 62/194) + changelog row.

Merge is the reviewer's action (the Windows lane is label-gated on CI: apply ci:windows before the run if the MSVC compile check is wanted — all new size_t→u32/u64 casts are explicit static_casts, audited for MSVC C4267/C4244).

offdev added 2 commits October 4, 2026 19:23
M2-SPRITE-04 scope (roadmap/M2-rendering-2.5d.md), nothing else:

- SpriteDrawStats: + programChanges, uploadBytes, renderTargetBytes,
  drawCallCapExceeded (the G-R2 frame-graph flag); SpriteDrawTotals:
  + the matching sums + capExceededFrames.
- Options::maxDrawCalls: the configurable per-pass draw-call cap
  (domain [1, kSpriteRendererMaxInstances], documented default
  kSpriteRendererDefaultDrawCalls = 64 = 2x the PRD §8.1 worst-case
  reference-scene budget of 30 draw calls). Exceeding it: one
  rate-limited Warn (draw_call_cap, fields capacity/draw_calls) + the
  frame flag + the total — the frame is STILL drawn (observation,
  never an execution gate; the G-R5 precedent).
- textureMemoryBytes(): the texture-memory VRAM estimate gauge
  (bound atlases' w*h*4 sum, updated at bindAtlas incl. re-bind
  replacement; 0 in the stopped state).
- A failed submit zeroes the frame counters (all three GL failure
  paths: upload, query creation, draw).
- New tests/laige-render/render_counters_tests.cpp + CTest entry
  render_counters (8 tests / 4 suites): the roadmap's known small
  scene (10 sprites, 2 atlases, 2 blends -> 3 groups) with the
  per-frame counters EXACT (frame 1 + frame 2, cross-frame state
  persistence) + totals, the empty frame counts nothing, the
  PRIMITIVES_GENERATED feed, the cap warn at the configured count
  (pinned fields, still drawn, no warn at the cap), and the VRAM
  gauge exact through binds + replacements. RenderCountersCreate is
  GL-free; the GL suites GTEST_SKIP on an environment failure.
- Docs: sprite_renderer.md (new observability + cap section),
  docs/README index, module README, roadmap box + board (M2 16/33,
  62/194) + changelog row.
- laige-api.json regenerated (1233 symbols / 38 headers, +12).

Verified: all six local trees warning-clean + full ctest green
(build 111/111, build-clang 111/111, build-release 100/100,
build-shared 111/111, build-asan 108/108); build-tsan 106/108 — the
two failures are a driver-internal teardown data race in this
machine's Mesa 26.2.3 llvmpipe (both race accesses inside libgallium;
the identical GL pattern passes in SpriteDrawSmoke.ThousandSpriteFrame
on this tree and on the CI TSan lane — the CI TSan lane is the
contract's TSan authority per the M2-GL-01 precedent).
api-real-tree/api-check-fresh, include-lint, determinism-lint green.
No budgets.json entry (O(1) bookkeeping; the composite 50k render
budget is M2-PERF-01). Additive only — no existing symbol changed.
…bled submit (TSan)

The CI TSan lane reproduced a driver-internal teardown data race
(Mesa llvmpipe: both race accesses inside libgallium — the
eglDestroyContext teardown destroys the driver's internal
mutex/condvar while the llvmpipe worker thread is still inside it),
triggered by a query-enabled submit as the FIRST FBO operation of the
frame.

Bisected locally (variants: clear-before-draw clean; 100 sprites,
16 384-pixel readback quiesce, and clear-after-draw all still race):
the frame-pipeline clear-before-draw pattern (the
SpriteDrawSmoke.ThousandSpriteFrame precedent) leaves the llvmpipe
teardown clean — verified 5/5 on this machine's Mesa 26.2.3.

The clear touches no sprite-pass state: the PRIMITIVES_GENERATED
feed, the frame counters, and the totals are unchanged.
build-tsan 108/108, all six local trees green.
@offdev
offdev merged commit d37b62f into master Oct 4, 2026
21 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant