Skip to content

[M1-LOOP-02] Presentation snapshot + interpolation state - #30

Merged
offdev merged 2 commits into
masterfrom
m1-loop-02-presentation
Sep 15, 2026
Merged

offdev merged 2 commits into
masterfrom
m1-loop-02-presentation

Conversation

@offdev

@offdev offdev commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Implements M1-LOOP-02 — Presentation snapshot + interpolation state (roadmap/M1-heartbeat.md): per tick, the world produces a presentation snapshot of Position2D state; render frames interpolate between the last two tick states with a clamped alpha.

What's here (the step's Scope, nothing else)

  • Position2D<Backend> — the first built-in component: the entity's 2D simulation-space position as the selected SimMath backend's Vec2 (ADR 0002). Both backends registered: Position2DFpx16 (fpx16_16, default) / Position2DFp32 (fp32_pinned, opt-in).
  • PresentationSnapshot<Backend> (new header-only public header, class template — one instantiation per backend, the M1-ECS-02 pattern):
    • onTick(tick): per completed tick, prev ← curr, curr ← world value for every live entity with a Position2D — over a pre-reserved per-slot table (24 B/slot, one setup-path allocation; no per-tick/per-frame heap, PERF-003).
    • New entities snap to curr (the documented scope behavior): entities added between ticks render at their spawn position, no phantom interpolation; generation-checked so slot recycling self-heals.
    • onRenderFrame(renderNs): the alpha alpha = (R − A(T)) × rate / 10⁹ in exact integer arithmetic (overflow-free for any 64-bit clock reading; no float accumulator, ARCH-010), clamped to [0, 1] — never extrapolates (before the anchor → 0; a clock jump a full tick or more past → 1).
    • sample_position(e) (the roadmap's exact name): lerp(prev, curr, alpha) for synced entities, snap for entities first seen since the last refresh, InvalidArgument + warn-once for stale handles (the World::check precedent), InvalidArgument no-warn for live handles without a Position2D, stopped (no world access, no log) when moved-from.
    • create(world, startReferenceNs, options): validates the tick rate (20–120 Hz, the loop's range) with one rate-limited presentation/tick_rate_invalid warn.
  • GameLoop gains the M1-HEAD-01 wiring seam: Options::onTick (plain noexcept function pointer — fires once per completed tick, never for a failed tick) + onTickContext + startReferenceNs() (the alpha's anchor base).
  • Docs (same change, CORE-006/DOC-007): new docs/api/presentation.md (full contract + Performance section), docs/api/game_loop.md (hook section, Options table, startReferenceNs), docs/README.md, the module README.
  • API manifest: laige-api.json regenerated (555 symbols; api-real-tree green).

Verify (step's Verify command: ctest -R presentation)

  • ctest -R presentation — green (10 cases: tick-rate validation; exact Q16.16 linear interpolation at alpha 0/0.5/0.75/near-1 (raw-unit expectations, no float round-trips); the alpha clamp matrix incl. a 285-year clock reading; entity-added-between-ticks snap-then-interpolate; catch-up per-tick refresh; stale/missing rejection; the GameLoop hook integration incl. the failed-tick case; moved-snapshot stop; the 500-entity × 100-frame zero-alloc window (allocs=0, machine-greppable); the fp32 backend).
  • Canonical g++ tree: zero-warning, full ctest 45/45.
  • build-asan, build-tsan, build-clang, build-release, build-shared: all zero-warning, 45/45 each (the sanitizer trees prove the zero-alloc window leak-free).
  • tools/laige-include-lint: OK.
  • Roadmap: box checked, Progress Board 12/25 (total 32/193), change log row — all in this PR.

Per-tick presentation state (M1-LOOP-02 scope, nothing else):

- Position2D<Backend>: the first built-in component (the entity's 2D
  simulation-space position as the selected SimMath backend's Vec2,
  ADR 0002); both backends registered (fpx16_16 default, fp32_pinned
  opt-in).
- PresentationSnapshot<Backend> (header-only class template, the
  M1-ECS-02 pattern): per-completed-tick prev/curr capture over a
  pre-reserved per-slot table (no per-tick/per-frame heap, PERF-003);
  the exact-integer anchored alpha alpha = (R - A(T)) * rate / 1e9
  clamped to [0, 1] - never extrapolates; sample_position(e) returns
  the SimMath lerp (synced entities), snaps entities added between
  ticks to their current value (documented), and rejects stale handles
  (warn-once) / live handles without a Position2D (no warn); moved-from
  snapshots are stopped (no world access, no logging).
- GameLoop: Options::onTick per-completed-tick hook (fires only for
  completed ticks) + startReferenceNs() - the M1-HEAD-01 wiring shape.
- Docs: docs/api/presentation.md (new, full contract + Performance),
  game_loop.md (hook + startReferenceNs), docs/README.md, the module
  README.
- Tests: tests/laige-sim/presentation_tests.cpp (suite Presentation;
  CTest entry 'presentation' = the step's Verify), 10 cases incl. the
  exact Q16.16 linear-interpolation values, the alpha clamp matrix,
  new-entity snapping, catch-up per-tick refresh, the GameLoop hook
  integration, and the 500-entity x 100-frame zero-alloc window.
- laige-api.json regenerated (555 symbols; api-real-tree green).

Verify: ctest -R presentation green; full ctest 45/45 on the canonical
g++ tree plus zero-warning 45/45 on build-asan, build-tsan,
build-clang, build-release, build-shared; tools/laige-include-lint OK.
@offdev
offdev merged commit 5c826f9 into master Sep 15, 2026
10 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