Skip to content

[M2-TILE-02] Parallax tile layers + tile animation - #80

Merged
offdev merged 1 commit into
masterfrom
feat/m2-tile-02-tilemap-anim
Oct 7, 2026
Merged

offdev merged 1 commit into
masterfrom
feat/m2-tile-02-tilemap-anim

Conversation

@offdev

@offdev offdev commented Oct 7, 2026

Copy link
Copy Markdown
Owner

M2-TILE-02 · Parallax tile layers + tile animation (FR-2.6)

Depends: M2-TILE-01, M2-PAR-01 (both merged). Scope only — nothing else.

Tile animation (data-driven frame cycling)

  • TileAnimationDef — frameCount (1..kTileAnimMaxFrames = 64), frameTicks (the documented rate: simulation ticks per frame, ≥ 1), the tile sheet's SpriteFrameLayout (M2-SPRITE-03, texels).
  • TileMap::Options::maxAnimations (1..kTileMapMaxAnimations = 256, default 8) — validated FIRST in create; the animation slot table is pre-sized at creation (id 0 = the static sentinel; 1..maxAnimations = animation slots). The tile's animationId is now consumed by the batch path (no longer inert data — existing tilemap goldens updated to static tiles).
  • setAnimation(id, def) — setup/config path (the parallax setLayer precedent): first-failure-wins validation (id domain → frame count → tick rate → sheet extents → frameCount ≤ sheet frames → tight-sheet float-exact domain with the adversarial-layout u64 overflow guards), no log, rejected sets leave the slot unchanged; a success resets the phase and precomputes all frame UVs (the only per-animation allocation).
  • advanceAnimations() — called once per sim tick (ARCH-002, never per render frame): every set animation's frame steps every frameTicks ticks, wrapping at frameCount (frame(ticks) = (ticks / frameTicks) mod frameCount). O(maxAnimations), zero allocation, no GL. Per-animation phase (all tiles of one animation in phase; per-tile offsets are the M3 editor's control). Frame state is presentation state (ARCH-009).
  • Declared quads: the static tile carries DeclareOptions::uv + frameIndex 0; the animated tile carries its animation's current frame UV (precomputed) + frameIndex. An animated tile whose slot is unset fails the declare (first failure wins).
  • setTile/rebuild gained the animationId ≤ maxAnimations check (whole span validated before any write).

Parallax tile layer (the M2-PAR-01 Tilemap-source hook, implemented)

  • New TileMap::declareTo(batcher, options, layers, layerId, cameraPos) — every quad translated by the layer's worldOffset(cameraPos) (the M2-PAR-01 formula (1)); its key is the M2-ISO-01 key of the translated center at the tilemap's own Options::layer (scene-setup convention: the def's depthLayer must equal the tilemap's layer — bg/mid/fg tilemaps get −2/−1/+1). Translated keys computed per tile per frame (camera-dependent — O(tileCount), zero allocation; the qBase + qOffset derivation is the documented upgrade path).
  • Protocol (first failure wins, nothing declared, no log): built frame / unset layer id / non-Tilemap-source layer → InvalidArgument; a disabled layer declares nothing (OK — the layer's documented skip).

Constraints honored

  • No GL anywhere (pure data + batcher bookkeeping — compiles in every tree, no GPU execution).
  • No per-frame allocation (FR-2.2): frame UVs precomputed at setAnimation, slot table at create — zero-alloc proof test: 1000 frames × (256 + 64 parallax tiles) of advance + declare + build under the owner-thread allocation window.
  • No new budgets.json entry (count stays 16): the per-frame cost is part of the composite 50k render-CPU budget (M2-PERF-01).

Tests — new ctest entry tilemap_anim (12 tests / 6 suites, both backends, no GL)

  • TileMapAnimSet — the setAnimation validation matrix (slot domain, frameCount [1,64] boundaries, tick rate, sheet domains, the adversarial tight-sheet overflow, rejected-set-leaves-no-state + phase reset) + no-log happy path.
  • TileMapAnimCycle — the frame cycle at the documented rate, hand-computed frame = ticks / frameTicks mod frameCount over two independent animations.
  • TileMapAnimDeclare — hand-computed frame-UV goldens (M2-SPRITE-03 tight-sheet formula), frameIndex, (atlas, material, blend) groups, the wrap, cross-frame bit-identical determinism.
  • TileMapAnimParallax — golden-verified offsets at given camera positions (factor 0.25, center (4,4), offset (1,2); camera (10,6) → offset (2.5,2.5)) + hand-computed layer (−2) key goldens (0x7FA00060/0x7FA00070 — the 2^21 bias adds into bit 21) + the independent oracle (isoDepthKey on the translated centers, both backends) + layer-dominance ordering + the protocol paths + the animated tile under the translation.
  • TileMapAnimProtocol — the animationId edit/load domain (out-of-range rejected, no state change) + the unset-slot declare failure.
  • TileMapAnimZeroAlloc — the 1000-frame advance + declare zero-allocation loop (standalone + parallax).

Existing tilemap suites updated to the new static-sentinel semantics.

Verification

  • All six local trees warning-clean: build 114/114, build-clang 114/114, build-release 103/103, build-shared 114/114, build-asan 111/111, build-tsan 111/111 (prior counts +1 each — the new tilemap_anim entry).
  • ctest -R tilemap_anim green (12/12); ctest -R tilemap green (the unanchored regex also selects tilemap_anim — intended, documented in CMakeLists).
  • laige-api.json regenerated last (1315 → 1342 symbols / 40 headers); api-real-tree/api-check-fresh, tools/laige-include-lint (69 files), tools/laige-determinism-lint (28 files, 0 violations) all green.
  • Docs in the same patch: docs/api/tilemap.md (major), docs/api/parallax.md + parallax.h hook comments, docs/concepts/coordinates.md (§4.9/§4.10/§5), docs/README.md, src/laige-render/README.md.
  • Roadmap box checked; progress board M2 19/33, total 65/194; change-log row.

Tile animation: TileAnimationDef (frameCount, frameTicks, the M2-SPRITE-03
sheet layout), the pre-sized animation slot table (id 0 = static sentinel;
1..maxAnimations), setAnimation (setup path: validation, phase reset, frame
UV precomputation), advanceAnimations (once per sim tick, ARCH-002;
frame = ticks / frameTicks mod frameCount), hasAnimation/animationAt/
animationFrame, the declared quad's current frame (static fixed UV or the
animation's frame UV + frameIndex), and the animationId domain in
setTile/rebuild (0..maxAnimations; whole span validated).

Parallax tile layer: TileMap::declareTo overload for a ParallaxLayers
Tilemap-source layer (M2-PAR-01 hook) — quads translated by the layer's
worldOffset(cameraPos), keys the M2-ISO-01 keys of the translated centers at
the tilemap's own layer (the def's depthLayer must equal it — scene-setup
convention); built frame / unset id / non-tilemap source -> InvalidArgument;
disabled layer declares nothing (OK).

No GL, no per-frame allocation (FR-2.2), no budgets.json entry (part of the
composite 50k budget). Tests: new ctest entry tilemap_anim (12 tests / 6
suites, both backends — validation matrix, documented-rate cycle, frame-UV
goldens, golden parallax offsets at given camera positions + oracle,
protocol, zero-alloc loop); tilemap_tests updated to the static-sentinel
semantics. Docs: tilemap.md (major), parallax.md, coordinates.md,
docs/README, module README, roadmap board + change log. laige-api.json
regenerated (1315 -> 1342 symbols).
@offdev
offdev merged commit 7e1135a 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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant