Repository navigation
[M2-PART-02] Particle rendering through the batcher - #83
Merged
Merged
Conversation
NEW laige::render::declareParticles<Backend> (header-only public header src/laige-render/include/laige/render/particle_render.h, templated over the SimMath backends): the FR-2.7 render half — every LIVE particle of a ParticleSystem<Backend> (M2-PART-01, read-only) becomes one SpriteItem declaration in the sprite batcher's frame window (M2-SPRITE-01): the O(live) particle-to-sprite conversion pass, zero allocation (PERF-003 — FR-2.7 "pooled"), no GL. Per particle: the world position at the RENDER-006 boundary (one documented rounding per component per backend); the engine-owned depth key (G-R11) — the particle's constant depth is first quantized to an INTEGER step height by particleDepthToStepHeight<Backend> (clamp to ±2047 FIRST — fpx raw integer compares, fp32 IEEE compares, NaN falls through to +2047 — then round nearest ties-to-even: fpx fpx16_16::toInt32, fp32 std::nearbyint; the two backends agree on dyadic depths, the presentation-only zone per ARCH-009 — NOT llround, which would diverge on 0.5) — then packed by isoDepthKey (M2-ISO-01) at the options' layer (default ground); depthOverride false (engine-owned). The square scale (size, size); the tint (u8→float /255 RGB + the M2-PART-01 exact-u32 fadeAlpha /255); the shared uv/atlasId/ materialId/blend from ParticleDeclareOptions (default BlendMode::Additive — the particle convention); frameIndex/rotation 0 (no per-particle animation or rotation in this step). Batching (RENDER-001): one emitter set = one system = one (atlasId, materialId, blend) group = one instanced draw call — 10 000 particles of one set cost one draw call. Declaration order = the live-array order (spawn order with swap removal) — the deterministic tie-break within equal keys (RENDER-003). Failure protocol (the first failed add fails the call): a stopped batcher → BudgetExhausted (an empty system still succeeds — no adds attempted); a closed window (after build) → InvalidArgument; a frame-budget overflow applies the batcher's own drop-oldest + warn policy (the conversion itself succeeds). Build: laige-render links laige-sim PUBLIC — the module stack's legal downward direction (PRD §10.1: render MAY include sim public headers; the include-lint R2 edge). The M2-GL-02 "no render→sim edge" note covers the frame-pipeline handoff, which stays an opaque pointer. Budget — NEW budgets.json entry particle_render_10k (step-level — the depth_sort_10k precedent; no PRD revision): the 10k-particle conversion pass ONLY (beginFrame/build outside the measured window — the batcher's sort is the separate depth_sort_10k budget), 100 warm-up + 3000 measured declare passes, both backends (the gate is backend-complete, ADR 0002). Target 2.0 ms (mean); recorded 1.07798 ms (the worse of the two backends, canonical Debug tree) — 1.86× inside the gate; the clang -O0 cross-run clears it at 1.18578 ms (1.69×). Gated on the Linux non-instrumented trees (LAIGE_PARTICLE_RENDER_BUDGET + LAIGE_PARTICLE_RENDER_BUILD_TYPE), ungated elsewhere (the sanitizer trees run the workload for its leak/race value). Tests — NEW ctest entry particle_render (9 suites, both backends, no GL): the per-particle declaration goldens against the M2-ISO-01 oracle + the hand-computed dyadic key goldens; the depth conversion (sub-unit ties-to-even, ±2047 saturation); the exact fade through the declaration; the in-group (key, live-order) ordering with the equal-key tie; the multi-set group count (3 sets → 3 groups — one draw call per emitter set); the failure paths; the fixed-seed determinism (the machine-greppable declared-sequence hash line; a different seed diverges); the 1 000-frame 10k-particle zero-allocation declare loop (the alloc-watch gate); the budget gate (including a workload-shape assert: all 10 000 alive). Docs: NEW docs/api/particle_render.md (the full contract + the DOC-004 Performance section); docs/api/particles.md (the render half now implemented — link + the render-phase example + Related); docs/concepts/coordinates.md (§4.11 narrative + the §5 conversion row Planned → Shipped + Related); docs/README.md (the API index); src/laige-render/README.md (the M2-PART-02 status paragraph). NEW baseline docs/benchmarks/baselines/m2-particle-render.md (the tenth, verbatim reports + the cross-compiler clang -O0 run) + the baselines index entry; budgets.json entry count 16 → 17 (the BudgetHarnessTable.LoadsTheRepoBudgetsFile pin updated). Build fix: the system clang 22.1.8 → 23.1.1 upgrade surfaced -Werror,-Wunused-but-set-global on pre-existing sim test files — removed the dead MemorySink* sink globals in game_loop_tests.cpp + presentation_tests.cpp + system_timing_tests.cpp (written, never read — the tests use installCaptureSink()'s return value) and LAIGE_ALLOC_COUNTER-gated the schReadPosSum read-back in scheduler_tests.cpp (its only read site is the zero-alloc window test, which compiles out on the sanitizer clang trees). API surface: laige-api.json regenerated LAST (1410 → 1419 symbols, 41 → 42 headers); api-real-tree / api-check-fresh green after the regeneration. include-lint (71 source files) and determinism-lint (29 sim source files, 0 violations) green. Verification — all six local trees warning-clean + full ctest green: build 116/116, build-clang 116/116, build-release 105/105, build-shared 116/116, build-asan 113/113, build-tsan 113/113 (the prior counts +1 each — the new particle_render entry); ctest -R particle_render green (9/9, both backends). GL/GPU paths untested locally (no usable GPU — the suites are GL-free by design; the P0 CI runners carry the GL environment).
offdev
force-pushed
the
feat/m2-part-02-particle-render
branch
from
October 8, 2026 12:32
a3f4227 to
45351b7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
M2-PART-02 — the render half of FR-2.7: particles render as batched
sprites through the existing sprite batcher (M2-SPRITE-01); depth
from the particle depth value.
laige::render::declareParticles<Backend>(header-only,src/laige-render/include/laige/render/particle_render.h): theO(live) particle → sprite conversion pass — zero allocation
(PERF-003), no GL. One emitter set (one
ParticleSystem) = one(atlasId, materialId, blend)group = one instanced draw call(RENDER-001) — 10 000 particles of one set cost one draw call.
INTEGER step height (
particleDepthToStepHeight: clamp to ±2047FIRST, then round nearest ties-to-even — fpx
fpx16_16::toInt32, fp32std::nearbyint; the backends agree ondyadic depths) and then packed by
isoDepthKey(M2-ISO-01) at theoptions' layer (default ground);
depthOverridefalse (G-R11engine-owned).
alpha /255.
laige-renderlinkslaige-simPUBLIC — the modulestack's legal downward direction (PRD §10.1); the frame-pipeline
handoff stays opaque (the M2-GL-02 note is about that edge).
budgets.jsonentryparticle_render_10k(step-level, mean ≤ 2.0 ms — the
depth_sort_10kprecedent, noPRD revision): the declare pass ONLY (
beginFrame/buildoutsidethe measured window — the sort is
depth_sort_10k's). Recorded1.07798 ms (worse backend, canonical Debug tree) — 1.86×
inside the gate; the clang -O0 cross-run 1.18578 ms (1.69×).
-Werror,-Wunused-but-set-globalon pre-existing sim test files— removed the dead
MemorySink* sinkglobals (game_loop /presentation / system_timing tests) and
LAIGE_ALLOC_COUNTER-gatedthe
schReadPosSumread-back in the scheduler test (its only readsite compiles out on the sanitizer clang trees).
Docs: NEW
docs/api/particle_render.md; the tenth baselinedocs/benchmarks/baselines/m2-particle-render.md(+ index entry);docs/concepts/coordinates.md§5 conversion row Planned → Shipped;docs/api/particles.md(render half implemented);docs/README.mdsrc/laige-render/README.mdstatus; roadmap checkbox + board(M2 21/33, total 67/194) + change-log row.
laige-api.jsonregenerated LAST (1410 → 1419 symbols, 41 → 42 headers).
Verification
ctestgreen: build116/116, build-clang 116/116, build-release 105/105, build-shared
116/116, build-asan 113/113, build-tsan 113/113 (prior counts +1
each — the new
particle_renderentry).ctest -R particle_rendergreen (9 suites, both SimMathbackends, no GL): the per-particle declaration goldens (the
M2-ISO-01 oracle + hand-computed dyadic key goldens), the depth
conversion (sub-unit ties-to-even, ±2047 saturation), the exact
fade through the declaration, the in-group (key, live-order)
ordering, the multi-set group count (3 sets → 3 groups — one draw
call per emitter set), the failure paths, the fixed-seed
determinism (machine-greppable declared-sequence hash), the
1 000-frame 10k-particle zero-allocation declare loop, and the
budget gate.
api-real-tree/api-check-fresh,laige-include-lint(71source files),
laige-determinism-lint(29 sim source files, 0violations) — all green.
Untested locally: GL/GPU execution (no usable GPU on this machine —
the suites are GL-free by design; the P0 CI runners carry the GL
environment).