Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e097078
docs(spec): the Tiles wall as a persistent source (slice 2, #448)
claude Sep 12, 2026
e84c13b
docs(spec): composed sources use nullopt, not a struct split (owner r…
claude Sep 12, 2026
d1e99bb
docs(spec): correct two wrong facts the spec was approved on
claude Sep 12, 2026
d1d3f8a
docs(plan): plan 1 of 2 for slice 2 — one animator per Tiles wall (#448)
claude Sep 12, 2026
c1febac
chore(registry): carve SourceRegistry out of #419 onto main's line, u…
claude Sep 12, 2026
7bbc4f5
feat(registry): admit composed sources, and never claim a subscriptio…
claude Sep 12, 2026
bb61908
fix(registry): skip composed sources in retirement/availability, and …
claude Sep 12, 2026
7288aa3
feat(tiles): a wall owns its own animation, keyed by wall id
claude Sep 12, 2026
aa35f4f
fix(tiles): cover TilesWallSource::advance's reset-detection glue
claude Sep 12, 2026
9d63c8f
fix(tiles): one animator per wall, so a wall taken mid-animation is c…
claude Sep 13, 2026
c11862d
fix(tiles): review round 1 — idempotent reset, read-path phantom-wall…
claude Sep 13, 2026
f3a646a
fix(tiles): review round 2 — one advance per wall per tick, not two w…
claude Sep 13, 2026
c171559
fix(tiles): review round 3 — no wasted preview plan build, Program wi…
claude Sep 13, 2026
909d101
fix(tiles): review round 4 — remove the foreign-plan foot-gun, add a …
claude Sep 13, 2026
bfc08ed
feat(tiles): register the wall as a composed source, releasing it whe…
claude Sep 13, 2026
b2e8ce4
fix(tiles): review round 1 - falsifiable idempotence test, retry-on-f…
claude Sep 13, 2026
646d8d1
test(rig): make the fake engine's motion measurable by the soak's lum…
claude Sep 13, 2026
c98da24
fix(#448): refuse setSubscription on a composed source, bound the wal…
claude Sep 13, 2026
71e4d4d
docs: close the #448 slice-2 ledger, file the residual minors, update…
claude Sep 13, 2026
821f950
docs: record the finalizer-thread XAML release crash (#513) — mechani…
claude Sep 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 166 additions & 23 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ untracked. The tier is a proposal, not an owner ruling.
| ID | Item | Clause | Size |
|---|---|---|---|
| [T5.1](https://github.com/iamfatness/CoreVideoPro/issues/447) | Reconcile persistent sources with the rearch: which #419 foundations slices 2-3 build on (likely `SourceRegistry`, atomic Take, `DeliveredProgramPacket`) | 3 | S |
| [T5.2](https://github.com/iamfatness/CoreVideoPro/issues/448) | Tiles wall stops re-animating on the cut (persistent-sources slice 2, on T5.1) | 3 | M-L |
| [T5.2](https://github.com/iamfatness/CoreVideoPro/issues/448) | Tiles wall stops re-animating on the cut (persistent-sources slice 2, on T5.1) — **plan 1 of 2 in PR #511**: one animator per wall + the #419 `SourceRegistry` carve, its first real consumer (soak: 20 takes, 20 cut, 0 rebuilt). Plan 2 = the wall texture / one-layer slice, Metal + CPU parity, the PVW cell as the registry's first reader, `AtomicTakeCoordinator`. Residual minors: #512. | 3 | M-L |
| ~~T5.3~~ | ~~A clip entering Program cold-starts with a placeholder flash~~ — **moved to T1.11** (owner, 2026-09-11) | | |
| [T5.4](https://github.com/iamfatness/CoreVideoPro/issues/450) | OHG: redesign on screens first, integrate into existing tabs; cheap parity gaps (preview tally, gallery order, black/bars/FTB, on-air clock, nameplates) | 3 | L |
| [T5.5](https://github.com/iamfatness/CoreVideoPro/issues/451) | The scene canvas editor shows live GPU video (redesign, not a whitelist) | 3 | L |
Expand Down
784 changes: 784 additions & 0 deletions docs/superpowers/plans/2026-09-12-tiles-wall-one-animator.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions native/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ add_library(corevideo_native
src/core/BoundedAsyncLog.cpp
src/core/LockHoldGuardrail.cpp
src/core/MediaCore.cpp
src/core/SourceRegistry.cpp
src/modules/AsyncEncoderSink.cpp
src/modules/AsyncOutputSender.cpp
src/modules/OutputDestinationSupervisor.cpp
Expand Down Expand Up @@ -619,6 +620,8 @@ if(BUILD_TESTING)
tests/CompositorFramingTest.cpp
tests/ContractParityTest.cpp
tests/RouteSourcePolicyTest.cpp
tests/SourceRegistryTest.cpp
tests/SourceRegistryComposedTest.cpp
tests/EmptyRouteBlankTest.cpp
tests/OutputLifecyclePolicyTest.cpp
tests/MonitorShedPolicyTest.cpp
Expand Down Expand Up @@ -666,6 +669,7 @@ if(BUILD_TESTING)
tests/TilesAnimatorTest.cpp
tests/TilesLayerTest.cpp
tests/TilesMembershipTest.cpp
tests/TilesWallSourceTest.cpp
tests/TilesRenderPlanTest.cpp
tests/UvcCaptureSupportTest.cpp
tests/VstHostAbiTest.cpp
Expand Down
64 changes: 28 additions & 36 deletions native/src/compositor/TilesPlanAnimation.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,46 +9,37 @@ class TilesPlanAnimation {
public:
void reset() { animator_.reset(); key_.clear(); sampled_.clear(); }

// Carry a SETTLED wall from one bus to the other (live-show defect, owner
// report 2026-09-09: "I can't have a total rerender from what is in preview
// to program like it is loading for the first time").
//
// The wall key is sceneId + ":" + layerId and the layer id is derived from
// the scene id, so the gallery sitting settled in PREVIEW and the same
// gallery a Take puts on PROGRAM carry the IDENTICAL key — it is one wall
// continuing on another bus, not a new one. Without this, the program
// animation saw a key it had never held, reset its animator, and threw away
// spring positions and entry alpha that were fully settled an instant
// earlier on the other bus.
//
// Scoped so the two buses can never contaminate each other:
// * only on an EXACT key match (a different wall, or a wall the other bus
// never held, is refused and animates exactly as it does today),
// * only when the other bus's wall is SETTLED (every sampled tile atRest —
// mid-flight state belongs to the bus that is flying it),
// * the state is MOVED, and the source is reset — never aliased, so the
// next wall cued on the source bus starts clean.
// Returns true if the state was carried across.
bool adoptSettledFrom(TilesPlanAnimation& previous, const std::string& wallKey) {
if (wallKey.empty() || key_ == wallKey) return false;
if (previous.key_ != wallKey || previous.sampled_.empty()) return false;
for (const auto& tile : previous.sampled_) {
if (!tile.atRest) return false;
}
animator_ = std::move(previous.animator_);
sampled_ = std::move(previous.sampled_);
key_ = wallKey;
previous.reset();
return true;
}
// Release a wall that is present but not animating (or not present at all).
// Plan-free BY DESIGN (review round 4, Finding 1): the caller has no real
// plan to give this wall on this path, and passing a FOREIGN one (e.g. the
// program plan, on behalf of a preview wall that shares its object) would
// only be safe as long as advance() returns before ever touching `plan` —
// an invariant that lives in a different file from the call site depending
// on it, and silently breaks into on-air geometry corruption the moment
// advance() is reordered or gains code above its early return. Removing the
// hazard is cheaper than documenting it: this takes no plan and cannot ever
// read one. Idempotent, matching advance()'s early-return semantics
// exactly: an ALREADY-released wall (key_ empty) reports it did NOT reset —
// without this a caller that releases every tick regardless of presence
// would read "reset" forever, turning the generation into a tick counter
// instead of a restart signal.
bool releaseIfIdle() { const bool had = !key_.empty(); reset(); return had; }

void advance(modules::CompositorRenderPlan& plan, const std::string& wallKey,
// Returns true when this call RESET the animator (a departure/disable, or a
// different wall key arriving) - the caller's only truthful signal of "did
// this wall restart", with no std::function/allocation on the render tick.
// `plan` is read ONLY on this present-and-enabled path (target extraction +
// applyLatest at the end) — a caller with no real plan for this wall must
// use releaseIfIdle() above instead of passing one in, never a plan built
// for a DIFFERENT wall.
bool advance(modules::CompositorRenderPlan& plan, const std::string& wallKey,
bool present, bool enabled, double durationMs, double nowMs) {
if (!present || !enabled) { reset(); return; }
if (!present || !enabled) return releaseIfIdle();
// A DIFFERENT wall never inherits this one's geometry. (sampled_ is cleared
// too: the all-stale guard below would otherwise let a new wall's first
// frames be drawn at the previous wall's tile rects.)
if (key_ != wallKey) { animator_.reset(); key_ = wallKey; sampled_.clear(); }
bool didReset = false;
if (key_ != wallKey) { animator_.reset(); key_ = wallKey; sampled_.clear(); didReset = true; }
std::vector<TilesAnimationTarget> targets;
for (const auto& layer : plan.layers) {
if (layer.kind == "participant-video" && layer.layerId.rfind("tile:", 0) == 0)
Expand All @@ -64,9 +55,10 @@ class TilesPlanAnimation {
// fix exists to remove. Only a wall that has actually drawn tiles
// preserves them; a cold wall's first tick is untouched, so a genuinely
// new wall behaves exactly as it always has.
if (targets.empty() && !sampled_.empty()) return;
if (targets.empty() && !sampled_.empty()) return didReset;
sampled_ = animator_.sample(targets, nowMs, enabled, durationMs, plan.width, plan.height);
applyLatest(plan, wallKey);
return didReset;
}
void applyLatest(modules::CompositorRenderPlan& plan, const std::string& wallKey) const {
if (key_ != wallKey) return;
Expand Down
Loading
Loading