Skip to content

[M2-PAR-01] Parallax layers - #79

Merged
offdev merged 3 commits into
masterfrom
feat/m2-par-01-parallax-layers
Oct 7, 2026
Merged

offdev merged 3 commits into
masterfrom
feat/m2-par-01-parallax-layers

Conversation

@offdev

@offdev offdev commented Oct 7, 2026

Copy link
Copy Markdown
Owner

M2-PAR-01 · Parallax layers (FR-2.3)

The named background/midground/foreground layer model: parallax factor (0..1), world-space offset, UV scroll (auto or manual), blend — all rendered through the sprite batcher (S-5).

  • NEW laige::render::ParallaxLayers<Backend> (header-only, templated over the SimMath backends) — the layer registry with the bounded slot table + the per-frame protocol.
  • Exact offset formula (the roadmap's pinned contract): worldOffset(p) = factor * (p - center) + offset — pinned bit-exactly for dyadic values, both backends.
  • Documented render order (background first): the layer quads carry the M2-ISO-01 key's layer field (presets bg = -2, mid = -1, ground = 0, fg = +1; custom: any of [-512, +511]) — within a shared (atlas, material, blend) group the layer field dominates; across groups the scene's atlas-id convention orders the batches (background ids below, foreground ids above the world content).
  • UV scroll with the exact wrap at the texture boundary (wrap(x) = x - floor(x) — 1.0 → exactly 0.0), rendered through the 2×2 wrap split (one SpriteItem carries one UV rect — no wrap): up to 4 quads per layer, one draw call per layer group.
  • Tilemap-source layers are the M2-TILE-02 data hook (skipped by declareTo; size/uv not validated).
  • ARCH-009 presentation-only (reads the camera's presentation position, never sim state); no per-frame allocation (FR-2.2 — 1000-frame zero-alloc proof); first-failure-wins validation with one rate-limited parallax/layer_invalid warn per rejected def.
  • Tests: new CTest entry parallax — 12 tests / 6 suites, no GL (12/12 green; ctest -R parallax = the step's Verify).
  • Docs: new docs/api/parallax.md; coordinates.md §4.10 + §5/§6 rows; module README; docs index; stale refs in iso_depth_key/iso_depth_table/tilemap updated.
  • API: additive only; laige-api.json regenerated last (1265 → 1315 symbols, 39 → 40 headers).
  • Budget: no standalone budgets.json entry (count stays 16) — the per-frame declare cost is part of the composite 50k render-CPU budget (M2-PERF-01; the M2-SCENE-01 reference scene has 3 parallax layers within the 50k-sprite / ≤30-draw-call budget).
  • Local verify: all six trees warning-clean + full ctest green: build 113/113, build-clang 113/113, build-release 102/102, build-shared 113/113, build-asan 110/110, build-tsan 110/110; api/include/determinism lints green.

Roadmap board: M2 17/33 → 18/33, total 63/194 → 64/194; box checked; change-log row added. CI: one P0 OS per PR — Linux lane on the default event + the ci:windows lane (MSVC audit of the new header-only template — C4267/C4244 precedent).

…et, UV scroll (FR-2.3)

NEW laige::render::ParallaxLayers<Backend> (header-only, templated over
the SimMath backends — the presentation.h pattern): the named
background/midground/foreground layer model of FR-2.3.

Model — a layer is a WORLD-SPACE RECTANGLE (one texture — the Image
source — or one tilemap — the Tilemap source, the M2-TILE-02 hook,
data-only here) whose content position at camera position p is the
EXACT formula (pinned by tests, bit-exact for dyadic values):

    worldOffset(p) = factor * (p - center) + offset

factor in [0, 1] (one source of truth): 0 = fixed in world space
(maximum parallax, a close-by background), 1 = fixed on screen (no
parallax, the sky layer); center = the reference camera position
(default the world origin); offset = the world-space offset at
p = center. Everything is world space (PRD §4, RENDER-006); the
camera position is the presentation-side input (ARCH-009).

Render order (documented — background first) — the layer quads carry
the M2-ISO-01 key's LAYER field (engine-owned, G-R11): the presets are
kParallaxDepthLayerBackground = -2, kParallaxDepthLayerMidground = -1,
kIsoDepthGroundLayer = 0 (the ground), kParallaxDepthLayerForeground
= +1; a custom layer picks any value in the M2-ISO-01 domain
[-512, +511]. WITHIN a shared (atlas, material, blend) group the layer
field dominates the key — background quads sort before every ground
object and foreground quads after it, whatever the quads' v
(engine-guaranteed). ACROSS groups the draw order is the batcher's
group order (ascending (atlas, material, blend), RENDER-003) — the
scene's SET-UP assigns the layers' atlas ids background-below /
foreground-above the world content (the M2-TILE-01 convention).

UV scroll (auto or manual) — each layer carries a CURRENT UV OFFSET in
[0, 1)^2. Manual: setUvOffset (any finite value, wrapped to [0, 1)^2).
Auto: advanceScrolls() advances by scrollSpeed (UV units PER FRAME per
axis — frames are the presentation pace; frame-rate independence is
the caller's concern, the M2-CAM-01 lerp precedent), once per frame,
before the declarations. The WRAP is exact at the texture boundary:
wrap(x) = x - floor(x) (1.0 -> exactly 0.0; -0.25 -> exactly 0.75). A
scrolled layer renders through the 2 x 2 wrap split (one SpriteItem
carries one UV rect — no wrap): up to four quads per layer (q00, q10,
q01, q11; an empty-range quad is skipped), one draw call per layer
group (FR-2.1, RENDER-001).

Semantics — one owner (the sim/scene-owner thread; the M2-GL-02
cull/batch stage owns the render-side declaration); sim-phase writes /
render-phase reads (CONC-001); ARCH-009 headless-buildable,
presentation-only (never sim state, never replay state); no per-frame
allocation (FR-2.2 — the 1000-frame zero-allocation proof);
setLayer validates first-failure-wins (a rejection leaves the slot
unchanged + one rate-limited parallax/layer_invalid warn with the
failing field — LOG-004); the happy paths log nothing (LOG-002/003).

Tests — NEW tests/laige-render/parallax_tests.cpp (CTest entry
'parallax' = the step's Verify command; 12 tests / 6 suites, no GL —
runs in every tree): the create/stopped-state matrix; the setLayer
validation matrix (15 rejections with pinned warn fields, domain
edges, the Tilemap size/uv not validated, state-unchanged-on-rejection,
replace + scroll-reset); the EXACT offset formula (hand-computed
dyadic goldens, both backends agree; factor-0.3 linearity within
tolerance); the auto/manual UV scroll with the EXACT wrap at the
texture boundary; the golden declare (the 4-layer + ground scene,
camera (10,6), both backends: the 5 quads pinned field-by-field, the
hand-computed 32-bit keys (with the 2^21 base) against the
independent M2-ISO-01 oracle, the group draw order, the
layer-dominance orderings, cross-frame determinism); the scrolled
split (full/half/un-scrolled: 4/2/1 quads, the world rects tile the
rectangle exactly, the atlas sub-rect mapping, the offset layer, the
frameCounts pin); the frame protocol (built frame / stopped batcher /
stopped registry / disabled + tilemap-hook skips); the 1000-frame
zero-allocation loop.

Docs — NEW docs/api/parallax.md (the API table, the model + the exact
formula, the render-order section, the UV scroll + the 2 x 2 split
table, ownership/threading, the DOC-004 Performance section, the
misuse warnings, the performant example); docs/README.md index entry;
the module README status paragraph; docs/concepts/coordinates.md
(NEW section 4.10 + the section 5 conversion row + the section 6
render-order summary + the Related link); the stale 'land with
M2-PAR-01' references updated (iso_depth_key.md, iso_depth_table.md,
tilemap.md, the tilemap.h comment).

API surface — laige-api.json regenerated LAST (1265 -> 1315 symbols,
39 -> 40 headers: the 2 enums, ParallaxLayerDef + 16 fields + the
field-wise operator== (a defaulted == would be deleted — SpriteUvRect
has no operator==), ParallaxLayers + Options + 13 public members, the
6 kParallax* constants). Additive only.

Budget — no standalone budgets.json entry (the count stays 16 — the
BudgetHarnessTable.LoadsTheRepoBudgetsFile pin): the per-frame declare
cost is PART of the composite 50k render-CPU budget (PRD section 8.1,
sprites_50k_cpu — M2-PERF-01 measures the reference scene with the
parallax layers included; the M2-SCENE-01 reference scene has 3
parallax layers within the 50k-sprite / <=30-draw-call budget).

Local verification — all six local trees warning-clean + full ctest
green: build 113/113, build-clang 113/113, build-release 102/102,
build-shared 113/113, build-asan 110/110, build-tsan 110/110 (the prior
counts +1 each — the new 'parallax' entry); ctest -R parallax green
(12/12); api-real-tree / api-check-fresh / include-lint /
determinism-lint green.
offdev added 2 commits October 7, 2026 11:26
…ntees the float overload of std::floor; std::floorf is not guaranteed in std — the CI 'Determinism check' job's libstdc++ does not expose it)
@offdev
offdev merged commit efe6bb0 into master Oct 7, 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