Skip to content

fix(#448): one animator per Tiles wall — and the first real consumer of the #419 SourceRegistry - #511

Merged
iamfatness merged 20 commits into
mainfrom
spec/tiles-wall-persistent-source
Sep 14, 2026
Merged

iamfatness merged 20 commits into
mainfrom
spec/tiles-wall-persistent-source

Conversation

@iamfatness

Copy link
Copy Markdown
Owner

What this is

Slice 2 of the persistent-sources redesign (#448), and the first real consumer of the #419 realtime-architecture foundations — which is why SourceRegistry lands here rather than as an unwired island (docs/BACKLOG.md: a #419 foundation lands on main only with its first real consumer).

Spec: docs/superpowers/specs/2026-09-12-tiles-wall-persistent-source-design.md (owner-approved)
Plan: docs/superpowers/plans/2026-09-12-tiles-wall-one-animator.md (plan 1 of 2)

The defect

A Tiles wall cued in Preview and taken to Program loses its in-flight animation on the cut. Owner, live show 2026-09-09: "what I can't have is a total rerender from what is in preview to program like it is loading for the first time."

The first fix was a hand-over (TilesPlanAnimation::adoptSettledFrom), which could only move a settled wall — with two per-bus animators, mid-flight state had no correct owner. So a wall taken mid-animation still broke.

This replaces the hand-over with one animator PER WALL (core::TilesWallSource, keyed by wall id, shared by both buses). There is nothing to hand over, so the cut is continuous even mid-flight. adoptSettledFrom is deleted.

The narrative was backwards, and it mattered

A reset does not replay from alpha 0. TilesAnimator treats a reset animator's next non-empty sample() as an adoption — content already present, not entering — so the wall snaps to its final state: alpha pops to 1, mid-spring rects jump to settled. On air that reads as a wall that stops moving and jumps.

The consequence is not cosmetic: EXPECT_GE(alpha_after, alpha_before) is satisfied by a snap just as well as by continuity and catches nothing — the first draft of the headline test passed against the unfixed code. The falsifying assertions are the ones bounding the other side (post-take alpha stays below 0.9 for a tile that was mid-ramp; a tile already at opacity 1 keeps its mid-spring rect), and those are what was verified red by reverting. Corrected in the spec, the plan, CLAUDE.md and the test's own comment.

The #419 carve

SourceRegistry arrives on main's line unchanged (one commit), then gains Kind::Composed for sources the core renders rather than captures. The Tiles wall is the first one.

  • A composed source carries no SDK handle and never claims a subscription state. personId, externalId, availability, subscriptionRequested, subscriptionObserved are all nulloptnullopt means not applicable, never false. setAvailability/setSubscription refuse Composed outright.
  • It is ERASED, never tombstoned (removeComposed). Not a simplification — the only mechanism that works: setAvailability refuses Composed, so nothing could ever flip it to Departed, and a nullopt availability makes externalConflict's != Departed test read true permanently, so a tombstoned wall id could never be reused.
  • Lifetime is scene-reference, in lockstep with TilesWallSources::releaseAllExcept one level down.
  • Honest scope: a production writer, no production reader yet. The only consumers are tests. The first real reader is the multiview PVW cell, in plan 2. Said so in CLAUDE.md.

Verification

Gate Result
Native suite (Release core, 2,180,096 bytes) 978 passed, 0 failed
mac-show-drill.py --seconds 40 --load 8 SHOW DRILL PASSED — 100% of decoded frames delivered, 0 overwritten, 0 starved; program 2.5–2.8 ms of 16.7 ms
live-meeting-soak.mjs --minutes 5 --takes 20 --sources 8 LIVE-MEETING SOAK PASS20 takes, 20 cut, 0 rebuilt over a shared 8-member wall; 60.0/s render, 0 deadline misses, 0 dropped video, 0 underruns
MonitorRenderFaultInjection (COREVIDEO_TIMING_TESTS=1) holds at baseline — 121/124/4 raw; shed 118/118/9 at divisor 2; recovered 182/182/6

Two measurement traps this branch fell into and fixed, both worth reading:

  1. A false regression, caused by the build command. A drill reported coreMutex over-budget 1% → 81% and I reported it to the owner as a real regression. native/build-dev is a multi-config generator with CMAKE_RUNTIME_OUTPUT_DIRECTORY pinned for every config, so Debug and Release write the same exe path and a missing --config Release silently installs a Debug core (8,322,560 bytes vs 2,168,832). The tell was uniform inflation across trivial stages (emit 32×, plan 19×). With --config Release every metric matched baseline. CLAUDE.md claimed the opposite ("single-config generator") and now documents the check.
  2. The soak's motion gate was unfalsifiable. It requires frame-to-frame YAVG > 0.05 — because 8995 frames of flat luma once passed every validator that counted frames — but the fake engine's "motion" was a one-pixel row, moving the frame mean by ~0.0007. A rig delivering a measured 60.0/s scored 0% of frames moving, so the gate could never pass on the only engine that can drive this soak headlessly. The fake engine now pulses a block (h/8 rows, 7 luma/frame ≈ 18× the threshold) at 1/8 the cost of the full repaint it deliberately avoids. movingFrameRatio 0.0 → 0.998.

Not done here (plan 2)

The wall texture / one-layer compositor slice, Metal + CPU-preview parity, the PVW cell as the registry's first real reader, and AtomicTakeCoordinator. The shared-wall coupling is the deliberate trade this slice accepts and is commented at both multiview cell sites: one wall id has one animator, so a gallery cued in Preview and live on Program shows Program's wall geometry in the PVW cell rather than an independent Preview animation. Program is the authority (enabled = programEnabled), applyLatest is a pure read, and making the cut continuous is the entire point of #448.

🤖 Generated with Claude Code

https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ

Answers #447 (T5.1) by naming exactly which #419 foundations slice 2 consumes,
and makes #448 the first real consumer so those foundations can land on main
wired rather than as an unwired island.

The residual defect: adoptSettledFrom only hands a wall across when every tile
is atRest, so a wall taken MID-ANIMATION still re-animates. With two per-bus
animators there is no correct answer to that, which is why the fix is structural
-- one animator per wall, one texture, buses sample it.

Owner decisions recorded in the spec: the wall registers in SourceRegistry under
a composed kind (not a capture kind, whose subscription/availability fields are
meaningless for a wall); the plan carries ONE tiles-wall layer rather than
collapsing expanded tile layers inside the D3D adapter, which pulls some Metal
and CPU-preview parity forward deliberately; and the multiview PVW cell moves to
sampling the Preview bus texture the way the PGM cell already samples Program.

Identity was verified rather than assumed: the shell already emits
tiles:<sceneId> (TilesLayerPayloadBuilder.cs:50) and the core takes it verbatim
(MediaCore.cpp:1808), so dropping today's redundant sceneId prefix preserves the
exact equivalence classes.

Gates are recorded with their honest state: the iGPU budget number CANNOT be
produced here (needs #425) and Metal will compile and pass CI but not be
executed. Neither is quietly skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
…uling)

The first draft said the composed kind "carries only Token, displayName, format
and publication state", which implied splitting SourceRegistry::Source into a
common core plus a capture payload. Investigating the cost changed the answer.

SourceRegistry::Source is one struct with a kind discriminator, and about five
of its fields are meaningless for a wall: no personId, no externalId (there is
no SDK handle), it cannot be Departed (a wall is released when unreferenced),
and it is never subscribed.

Two findings made this more than cosmetic:

- The defaults are ASSERTIONS. subscriptionObserved{false} is initialised
  engaged, and its own comment says nullopt is what means unknown -- so a
  registered wall would assert "subscription observed = false". That is the
  shape of #468 and of "absent lifecycle means UNKNOWN, never healthy".
- This is a PUBLISHED, golden-tested contract, not an internal struct:
  test/data/wave1-authority.json is authority-goldens-v2 with ~1,700 lines of
  scenarios, and a serialized source carries personId, videoAvailable,
  videoFresh, audioAvailable, audioFresh, audioMuted. Registering a wall forces
  an answer to "is this wall's audio muted?".

Splitting the struct was therefore rejected on cost: it touches every existing
#419 consumer and its tests and reshapes those goldens -- #419 surgery smuggled
in under a Tiles-wall fix, when the owner asked for a minimal carve. Leaving the
fields as-is was rejected too: a field that serializes a false claim is how #468
happened.

The fields become std::optional and stay nullopt for composed sources, the
serializer omits them, and two new tests pin it: a wall never claims a
subscription state, and the existing capture goldens stay byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
Found while gathering detail for the implementation plan. Both errors were mine,
and one of them was the argument AGAINST the option I talked the owner out of.

1. The spec claimed SourceRegistry::Snapshot is a published, golden-tested wire
   contract. IT IS NOT. That snapshot is consumed in-process by
   ShowPlanGenerator and SceneVersionShadow and never reaches the wire or the
   session state. The authority-goldens-v2 scenarios belong to
   ZoomSourceAuthorityAdapter::Observation::Source -- the Zoom ROSTER OBSERVATION
   synced INTO the registry, carrying videoFresh/audioMuted/personId. A wall
   registered directly in SourceRegistry never appears there.

2. Because of (1), the stated cost of splitting the struct ("reshapes ~1,700
   lines of goldens") was fabricated. Measured: about seven files in the Zoom
   authority path plus tests. Real but modest.

The conclusion still stands after re-deciding with correct facts (owner: keep
the nullopt approach), but on NARROWER grounds: subscriptionObserved{false} is
initialised engaged while its own comment says nullopt means unknown, so a
registered wall asserts "subscription observed = false" -- and that feeds PLAN
GENERATION. The split is deferred to slice 3, where more than one composed kind
would justify the shape.

Also records a finding that applies whichever option is chosen:
validRegistration requires a non-empty externalId, so add() returns Invalid for
a wall today, and externalConflict matches kind+processEpoch+externalId, so two
walls with empty external ids would collide. Both now have named tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
Four tasks, each ending in an independently testable deliverable:
  1. SourceRegistry admits a composed source and never claims a subscription
     state for it (both current rules reject a wall today: validRegistration
     demands a non-empty externalId, and externalConflict would merge two walls
     that both have none).
  2. TilesWallSource owns one wall's animation, keyed by wall id.
  3. MediaCore uses one animator per wall and adoptSettledFrom is DELETED.
     This is the task that fixes #448.
  4. The wall registers as a composed source and the take record proves its
     continuity with a generation.

Split from the spec deliberately. Plan 1 changes NO drawing, so it can land
while beta-2026-09-12-c425e3c is still being live-checked; plan 2 (wall texture,
one tiles-wall layer, Metal/CPU parity, PVW cell, AtomicTake) changes the render
path and should wait for that beta to be shaken out. Each plan produces working,
testable software on its own.

Self-review caught a type inconsistency worth recording: the first draft had
TilesPlanAnimation calling a std::function installed by TilesWallSource, which
Task 2 never installed AND which would allocate on the render tick. Replaced by
advance() returning bool "did I reset", which the wrapper turns into a
generation bump -- so the generation cannot move without the animator moving,
and vice versa.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
…nchanged

Prerequisite for plan 1 of persistent-sources slice 2. No behaviour change: the
files are taken verbatim from origin/codex/production-realtime-architecture and
wired into the existing targets.

The carve is clean because SourceRegistry.cpp includes only its own header plus
<stdexcept> and <utility> -- zero project dependencies -- which is the condition
the parent spec set ("SourceRegistry.h is reused only if it fits cleanly").
Nothing else from #419 is needed: SceneVersionShadow and ShowPlanGenerator
consume the registry but are not part of this plan.

Verified: its own 22 tests pass, and the full suite is 953 green (931 + 22).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
…n state for them

Also fixes a dangling-reference bug in the ACaptureSourceStillCarriesItsCaptureFields
test: `registry.snapshot()->sources.front()` as one expression drops the temporary
shared_ptr (and its Snapshot) at the semicolon, leaving `source` a dangling
reference. Bind the shared_ptr first, same pattern already used by the other new
test in this file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
…narrow the composed conflict bypass to externalId

Fix round 1 review findings:

1. retireProcessEpoch and setAvailability reached a composed source's fields
   unconditionally, reintroducing the false claim install() was fixed to
   remove (nullopt flipping to concrete Departed/false). Both now SKIP a
   composed source instead of marking/mutating it: retirement has nothing to
   fence (no provider process, no callbacks) and availability does not apply
   to a wall at all. Not erased, not marked Departed - a wall's lifetime is
   scene-reference, released by a separate sweep in a later task.

2. externalConflict's composed bypass disabled the instanceId collision check
   too, not just the externalId one. Restructured so only the externalId
   clause is skipped for a composed registration; two composed registrations
   sharing an explicit instanceId still conflict.

Adds 3 covering tests, two confirmed red (via a temporary guard revert) before
the fix and green after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
TilesWallSource wraps TilesPlanAnimation and bumps a generation whenever
the wrapped animation reports it reset, giving the take record's proof
that nothing restarted (spec section 5) as a number instead of an
opinion. TilesWallSources maps wall id -> source so both buses asking
for the same wall get the same object; lifetime follows "referenced by
a scene" via releaseAllExcept, and a recreated wall starts at
generation 0 rather than continuing a retired one's count.

TilesPlanAnimation::advance now returns bool (true when it reset the
animator) instead of void, so TilesWallSource::advance can detect a
reset with no std::function/allocation on the render tick. Every
existing caller (MediaCore, TilesAnimatorTest) already discards the
return value as a statement, so this is a source-compatible widening.

Not wired into MediaCore yet - that is the next task.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
Review round 1 finding 1: the original 5 tests never called
TilesWallSource::advance - they only exercised noteReset()/generation
bookkeeping directly, leaving the actual glue (bump the generation iff
the wrapped TilesPlanAnimation::advance reports a reset) completely
untested. Add two tests driving advance() directly: a different wall
key arriving moves the generation by exactly one (the reset path), and
the same wall key arriving twice does not move it. Verified both are
load-bearing by temporarily breaking the glue two ways (unconditional
noteReset(), and an inverted condition) and confirming each break turns
at least one test red before restoring the fix.

Finding 2: add the missing <iterator> include for std::next, previously
compiling only because MSVC's <map> transitively pulls it in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
…ontinuous

MediaCore held programTilesAnimation_ and previewTilesAnimation_, and handed
settled state from one to the other on a Take via
TilesPlanAnimation::adoptSettledFrom. That hand-off refused a wall whose tiles
were still flying (mid-flight state had no correct owner with two animators),
so a wall taken mid-animation re-animated its entrance from alpha 0 on
Program.

MediaCore now keeps ONE core::TilesWallSource per wall id (tilesWallSources_,
from Task 1/2), shared by whichever bus is looking at it, so there is nothing
to hand over. adoptSettledFrom is deleted. TilesPlanAnimation::advance already
reports whether it reset (bool return); TilesWallSource turns that into a
generation bump.

TakeRecordPolicy's wallAdoptedSettled is renamed wallContinuous: it no longer
means "state was handed over" but "the wall's generation did not move across
this take", computed in MediaCore by comparing tilesWallGeneration() before
and after the take-tick's advance. The verdict string "adopted-settled"
becomes "continuous".

Rewrote the three TilesAnimatorTest.cpp hand-off tests that pinned the old
per-bus behaviour: a wall settled on one bus is now trivially settled on the
other (same object); the refused-hand-off case becomes "a wall never cued
starts cold, at generation 0, in its own object, contaminated by nothing else";
the all-stale-beat guard test is kept as-is (it already tested the shared
animator directly, and matters more now that a wipe hits every bus at once).

Added the headline regression test (TilesRenderPlanTest.cpp,
AWallTakenMidAnimationIsContinuous). A wall's own first tick with content pops
in fully opaque by design (TilesAnimator treats a fresh animator's first
non-empty sample as pre-existing, not entering — see
DifferentWallCannotReuseAnotherWallsCachedGeometry), so the test instead joins
a member onto an ALREADY-ESTABLISHED wall and takes it before the join
settles. Verified red-before-green by temporarily restoring the base
(aa35f4f) per-bus mechanism: the same scenario failed with the new tile
snapping straight to full opacity instead of continuing its entrance,
confirming the test exercises the real defect.

Closes #448

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
…, freeze fix, verdict/rect assertions

Fixes five review findings on the #448 one-animator-per-wall change:

- Finding 0: TilesPlanAnimation::advance's not-present/disabled early return
  now reports whether it actually reset (key_ was non-empty), not "true"
  unconditionally — a repeated disabled tick is a harmless no-op, not a
  generation tick-counter.
- Finding A (Critical): the preview GPU-composite applyLatest call used
  forWall(), inserting a phantom ""-keyed wall for any preview scene with no
  wall, under coreMutex, every render tick. Switched to find() + null-check,
  matching the two multiview sites.
- Finding B: the double-advance guard keyed on wall-id equality alone, so a
  wall with animateLayout=false on Program and true on Preview at the SAME id
  was advanced by neither branch (frozen). Hoisted `programAdvanced =
  present && animateLayout` and gated preview's advance on
  `!programAdvanced || previewWallId != programWallId`.
- Finding C: advance() was only called when animateLayout was true, so a
  disabled wall's sampled_ was retained forever and kept feeding stale
  geometry to the multiview PGM cell and the preview composite. Restored the
  unconditional call whenever the wall is present, letting `enabled` decide
  reset-vs-sample inside advance() (safe now under Finding 0's guard).
- Finding D: added an end-to-end assertion in the headline test that the take
  record itself reads wall=="continuous", verdict=="cut" — not just the raw
  generation number.
- Finding E: added rect-continuity assertions for tiles whose opacity was
  already 1 at the mid-flight tick (only the newly-joined tile had a ramping
  alpha; the pre-existing two tiles' RECTS were mid-spring from the layout
  change, unasserted before this).

Self-caught while implementing Finding C: widening the advance gate to
"present" alone broke ATakeIsRecordedWithBothSidesOfTheWall — a wall id seen
for the first time starts at generation 0, and with Finding 0's idempotence a
disabled first call also reports "did not reset", so raw generation equality
misread a brand-new non-animated wall as continuous. Fixed by requiring
wall EXISTENCE (find() before forWall()), not just generation equality.

Self-caught while implementing Finding E: the rect-continuity check first
failed against the CORRECT implementation too, because the shared wall's
100ms clamped-floor duration converges audibly close to target within a few
ms of real test-process overhead. Added wallSceneWithDuration() and drove the
test at the animator's 2000ms clamp ceiling for a reliable margin.

Re-verified red against the reverted base mechanism with both new teeth
(Finding D's take-record assertion and Finding E's rect check both fail);
re-verified green on the fixed tree, 968/968, three consecutive runs with no
flakiness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
…ith contradictory enabled

Re-review confirmed round 1's wallExistedBefore term and the 2000ms tolerance
retune were both sound. It found one new Important finding introduced by
Findings B and C combining: a wall present on both buses with DISAGREEING
animateLayout (Program false, Preview true, or vice versa) was advanced TWICE
in one tick on the SAME shared object with contradictory `enabled` values —
Program's call (enabled=false) reset it, then Preview's separate call
(enabled=true) saw an empty key and reset it AGAIN. Net per tick: generation
+2, no animation ever completing, the object re-adopted from scratch every
tick. Not a verdict inversion (it read the conservative "rebuilt"), but
incoherent under the one-animator-per-wall model this task exists to
establish.

Restructured the take-tick block so exactly one advance() call happens per
wall per tick, true by construction:
- `sameWall` decides whether Program and Preview name one object or two.
- When they share an id, `enabled` is the OR of both buses' animateLayout —
  a wall animates if EITHER bus wants it to — and there is exactly one
  advance() call to carry that decision.
- Preview only gets its own separate advance() call when it names a
  DIFFERENT wall id (a genuinely different object).

Kept everything the round-1 re-review confirmed: the `had` idempotence guard
in TilesPlanAnimation::advance, find() on every read path, the
wallExistedBefore existence check, and the unconditional-whenever-present
call shape that fixed Finding C (now applied symmetrically to both branches).

Added TilesRenderPlan.ASharedWallWithDisagreeingAnimateFlagsAdvancesAtMostOncePerTick
— no prior coverage exercised this configuration, which is why it took two
review rounds to surface. Verified red against c11862d (generation climbed
every tick) and green after this restructure, across three consecutive full
suite runs (969/969, 0 failed).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
…ns for a shared wall, drop empty-id exemption

Three fixes from re-review of round 2's restructure:

Finding 1 (performance): the preview branch's `!sameWall` gate lost its
`animateLayout` check, so buildPreviewCompositorRenderPlan(videoFrames) — a
full buildRenderPlanForScene deep build, ~13 strings per layer, plus the
paused-clip-cue pass — ran every render tick under coreMutex for any
disabled preview wall (the DEFAULT) with a different id from Program, then
was thrown away: advance() returns at its very first line when !enabled and
never touches the plan. Same class of waste the "Task 4 review fix (I6)"
comment a few lines below already exists to prevent. Now the deep plan is
only built when previewEnabled is true; the disabled path passes the
already-built program `renderPlan` instead (advance() never reads it there).

Finding 2 (behaviour, RULING): `enabled = programEnabled || previewEnabled`
let a PREVIEW-side animateLayout toggle start motion on PROGRAM for a shared
wall, with no take involved, because the shared advance samples straight
into the program renderPlan. This codebase's bedrock rule is that an off-air
Preview look never changes what is on air (CLAUDE.md: "an off-air Preview
look can never take video ... from a Program source") — the preview scene is
an operator-editable draft (S2b), so a draft edit reaching Program is a
live-show hazard, and Program is inherited by the vcam, every recording and
every stream. Changed to `enabled = programEnabled` — Program always wins
for a shared wall. Cost-free thanks to round 1's Finding C: a disabled
shared wall still resets (idempotently), sampled_ clears, and applyLatest
no-ops on the mismatched key_, so Preview simply falls back to raw
(non-animated) geometry rather than stale rects — nothing on air moves
because of an off-air edit.

Finding 3 (minor): dropped the `!programWallId.empty()` term from `sameWall`
— two empty ids are the SAME map entry (TilesWallSources keys on the
string), so excluding them re-opened the exact two-advance-on-one-object
shape round 2 closed. No generation effect, but the wrong shape regardless.

Rewrote the round-2 test to assert the NEW property directly: with Program's
animateLayout=false, a wall shared with an animateLayout=true Preview never
leaves generation 0 — not "settles after climbing once" (round 2's OR
behaviour), which the rewritten test now catches if ever restored (verified
by temporarily reintroducing the OR: the test fails on tick 0, as expected).

Verified: covering suites green (TilesRenderPlan 29, TilesAnimator 11,
TakeRecord 11, RenderedSceneAttribution 3), full binary 969/969 across three
consecutive runs, check-string-escapes.py clean.

Deferred per coordinator instruction, NOT fixed: forWall() admitting an
empty wall id on the write path; the rect-tolerance/duration headroom.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
…real precondition

Finding 1 (Important, coordinator's own prescribed shape): the disabled
preview-wall branch passed the PROGRAM render plan into a PREVIEW wall's
advance() call. Correct today only because advance() returns at its very
first statement when !enabled — an invariant living in a different file
from the call site depending on it. If that early return is ever reordered
or gains code above it, applyLatest would silently rewrite rect/opacity on
every tile:* layer of the PROGRAM plan (the same object handed to
compositor->render() and cached into lastRenderPlan_): on-air geometry
corruption inherited by the virtual camera, every recording and every
stream.

Removed the hazard rather than documenting it further:
- TilesPlanAnimation::releaseIfIdle() — plan-free by construction, cannot
  ever read a plan because it takes none. Same idempotent generation
  contract as advance()'s early return (had = !key_.empty() before reset).
- advance()'s early return now just calls releaseIfIdle().
- TilesWallSource::releaseIfIdle() wraps it and calls noteReset() on a real
  release, matching advance()'s generation semantics exactly.
- MediaCore's disabled-preview branch now calls
  tilesWallSources_.forWall(previewWallId).releaseIfIdle() — no plan
  argument exists to misuse.
- Documented advance()'s `plan` parameter as read ONLY on the
  present-and-enabled path, so the next person knows releaseIfIdle() is the
  only correct call when there is no real plan for a wall.
- Left the all-stale early return (`targets.empty() && !sampled_.empty()`)
  untouched — it runs after the key_ assignment and was never part of this
  hazard.

Finding 2 (Minor): the round-3 test was entirely negative assertions
(generation == 0), which pass vacuously if the shared configuration itself
stopped existing. Added preconditions: Program's wall must actually render
"tile:zoom:1" (forced via the existing setTilesMemberFrameAgesForTest seam,
which only rebuilds lastRenderPlan_ and cannot disturb
tilesWallSources_/generation), and Preview's scene must have landed on the
SAME "tiles:s" wall id with animateLayout true — before the zero-generation
assertions run.

Verified: covering suites green (TilesRenderPlan 29, TilesAnimator 11,
TakeRecord 11, RenderedSceneAttribution 3), full binary 969/969 across two
consecutive runs, check-string-escapes.py clean. Generation semantics are
unchanged by the refactor by construction (releaseIfIdle IS the old early
return, extracted) — the round-2/3 tests pass for the same reason as before.

Still deferred per coordinator instruction: forWall() admitting an empty
wall id on the write path; the rect-tolerance/duration headroom.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
…n unreferenced

The wall is the first real consumer of SourceRegistry's Kind::Composed
(#448 slice 2, task 4). Registers as Composed the tick a wall becomes
live (sourceId = its layerId, externalId empty, a fixed core-process
epoch) and releases it the tick nothing on either bus names it any
longer, in lockstep with tilesWallSources_.releaseAllExcept — the same
"named by a live scene" lifetime, one level up. registeredWallIds_ is
the idempotence guard so a live wall's steady-state tick never touches
the registry mutex.

SourceRegistry had no way to actually remove a composed source (real
sources are tombstoned via retireProcessEpoch/setAvailability, which
explicitly SKIP composed sources with a comment deferring removal to
"a separate sweep (later task)"). Added SourceRegistry::removeComposed,
which erases outright (never tombstones) and refuses non-Composed
kinds - the wall has no provider process to fence, so a tombstone
would be a permanent, meaningless entry that also makes add() answer
Conflict forever for a freed wall id.

Per explicit ruling, this task does NOT touch TakeRecordPolicy or the
take record's sources[] - that continuity fact already exists via
wallContinuous, and a second representation of it is exactly what this
codebase forbids elsewhere (one status machine per fact).

Tests: SourceRegistryComposedTest.cpp (removeComposed: erases, frees
the id for reuse, refuses non-composed, NotFound on unknown) and
TilesRenderPlanTest.cpp (a live wall registers as Composed with all
five capture-only fields nullopt; an unreferenced wall is gone from
the registry; a released-then-re-cued wall gets a new instanceId; a
wall staying live across many ticks holds exactly one registry entry).
Full suite: 977 passed, 0 failed (was 969). Red-before-green verified
by reverting MediaCore.cpp/.h to HEAD with the new tests in place
(compile failure), then restoring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
…ailure, alloc-free by construction

Fix round 1 of the Task 4 wall-registers-as-composed-source review.

Finding 1 (Important): ALiveWallHoldsExactlyOneRegistryEntryAcrossManyTicks
asserted a count std::map makes structurally impossible to violate, and
was near-unfalsifiable - dropping the idempotence guard entirely (add()
answering Conflict on every tick, taking the registry mutex 60x/s on the
render path) still passed it. Replaced with
ALiveWallKeepsTheSameRegistryIdentityAcrossManyTicks, asserting the
wall's instanceId is identical across 25 ticks - the realistic
regression (remove-and-re-add every tick) mints a fresh instanceId,
which this catches and a count cannot. Verified genuinely falsifiable
by simulating that exact regression and confirming red, then restoring
and confirming green.

Finding 2 (Minor): a registration that failed (Invalid is reachable,
not just Conflict/Exhausted) was remembered as registered anyway, so it
was never retried while the wall stayed live. Now the guard id is only
added on Result::Applied, so the next tick retries a failed wall.

Finding 3 (Minor): registeredWallIds_.insert(id).second is not
guaranteed allocation-free on an already-present key on every
implementation (MSVC's unordered_set has historically built the node
before detecting the duplicate). Switched to contains()-then-insert so
the render-path no-allocation rule holds by construction.

Finding 4 (Minor): three SourceRegistry comment fixes - removeComposed
takes a bare SourceId deliberately (sole-owner lifetime, no replace()-
style fence); retireProcessEpoch's composed-skip comment names
removeComposed instead of "later task" and explains why a tombstone
was never actually reachable (setAvailability refuses Composed, and a
permanently-nullopt availability makes externalConflict's Departed
check permanently true); Snapshot::sources notes composed sources
vanish rather than tombstone.

Finding 5 (Minor, merge-blocking): added a CLAUDE.md section documenting
the composed-source erase-vs-tombstone asymmetry and both reasons for
it, per this repo's docs-updated-is-part-of-done rule.

Full suite: 977 passed, 0 failed (x2 consecutive). Escape scanner clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
…a gate

`live-meeting-soak.mjs` gates "did the picture move" on frame-to-frame YAVG
exceeding 0.05, because 8995 frames of flat luma once passed every validator
that counted frames. The fake engine's motion was a ONE-ROW band that brightens
row N and restores row N-1, which changes the frame mean by ~0.0007 — so a rig
delivering a measured 60.0/s scored 0% of frames moving, and the gate could
never pass on the only engine that can drive this soak headlessly.

A gate no harness can satisfy is unfalsifiable, which is worse than no gate.
Pulse a BLOCK instead: h/8 rows stepping 7 luma per frame is ~0.9 YAVG at the
frame level, 18x the threshold, at 1/8 the cost of the full repaint this
function deliberately avoids (that repaint made the PRODUCER the bottleneck at
~22fps/source). Offset by participant id so a wall's tiles carry different
values while all advancing together.

Measured after: 5 minutes, 20 takes, 8 sources — movingFrameRatio 0.0 -> 0.998,
LIVE-MEETING SOAK PASS, with the take property intact (20 cut, 0 rebuilt) and
60.0/s render, 0 deadline misses, 0 dropped video.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
…l-registration retry, correct the defect narrative

Final-review findings on the #448 slice, plus the documentation defect the
review surfaced.

setSubscription refused Kind::Composed nowhere, while CLAUDE.md asserted it did.
An observed:true call was already refused as a side effect (a nullopt
availability is not Available), but requested:true with observed nullopt applied
cleanly and turned a NOT-APPLICABLE field into a concrete claim — the exact lie
the nullopt fields exist to prevent. Nothing in the tree calls it for a wall, so
only the docs were wrong, which is how an invariant rots. Red-verified first.

The wall registration loop retried a PERMANENTLY invalid id forever. A failed add
is deliberately not remembered as registered so transient failures retry on the
next liveness transition — but an id refused on its SPELLING can never become
valid, so that retry became a registry-mutex acquisition plus an unbounded log
line on every render tick. SourceRegistry::kMaxIdBytes is now public (one
declared bound, used by validRegistration) so a caller can tell the two cases
apart; unregisterableWallIds_ skips the permanent ones once and loudly, and
warnedWallRegistrationIds_ bounds the retryable failures' line to once per id
while keeping the retry. Both are pruned on the same liveness rule as
registeredWallIds_.

THE DEFECT NARRATIVE WAS BACKWARDS in CLAUDE.md, the spec, the plan and the
headline test's own comment: a reset does not "replay from alpha 0".
TilesAnimator treats a reset animator's next non-empty sample() as an ADOPTION,
so the wall SNAPS TO ITS FINAL STATE — alpha pops to 1, mid-spring rects jump to
settled. The consequence is not cosmetic: EXPECT_GE(alpha_after, alpha_before) is
satisfied by a snap just as well as by continuity and catches NOTHING (the first
draft of that test passed against the unfixed code). The falsifying assertions
are the ones bounding the other side — post-take alpha below 0.9, and a tile
already at opacity 1 keeping its mid-spring rect — and those are what was
verified red. Documented at all four sites, with the honest limit that the
generation-equality check is not independently falsified by that revert.

Also: CLAUDE.md claimed build-dev is a single-config generator. It is Visual
Studio 18 2026 with CMAKE_RUNTIME_OUTPUT_DIRECTORY pinned for every config, so
Debug and Release write the same exe path and a missing --config Release silently
installs a Debug core. That cost a false regression report on 2026-09-12
(coreMutex 1% -> 81%); the tell was uniform inflation across trivial stages and
an 8,322,560-byte binary against Release's 2,168,832. Documented with the check.
Plus: the registry section now says it has a production writer and no production
reader yet, and the shared-wall applyLatest coupling is commented at both
multiview cell sites and filed to plan 2.

Tests: 978 passed, 0 failed on the Release core.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
… the backlog

The final gates re-run on the post-fix binary (978 tests, SHOW DRILL PASSED,
LIVE-MEETING SOAK PASS at 20 takes / 0 rebuilt), the soak motion gate's
unfalsifiability and its ruling, the environmental cause of the first soak
failure (we join as host, so killing the app ended the meeting), and the record
that the defect narrative was backwards in four places.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
…sm confirmed, trigger not reproduced

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yuH7EMvWCWdkhvevrtMyJ
@iamfatness
iamfatness merged commit 357d362 into main Sep 14, 2026
19 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.

2 participants