feat: ADR-W018 L2 — live add/edit of network sources (the DEV-stack spine) - #181
Merged
Conversation
…rk live add TDD-first: failing tests for network source live add/edit functionality. Tests expect APIs not yet implemented: - typed_resources.rs delta: LiveSourceCapability + with_live_sources + capability-driven header - live_apply_header_http.rs: bind_and_serve HTTP honesty (live only when spawner wired) - live_network_apply.rs (ffmpeg-gated): e2e real ingest loop + state machine + never-falter proof These tests pin the exact behavior: network/file kinds live on full-pipeline, ndi/youtube/aes67 always restart, capability derives from ingest.is_some(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k live-add
- config/schema.rs: add SourceKind::is_network_media() classifier (rtsp/hls/ts/srt/rtmp/file/rist)
- control/state.rs: add LiveSourceCapability struct {synthetic, network} with constructors + is_live(kind)
- control/state.rs: add AppState.live_sources field (default synthetic_only)
- control/state.rs: add AppState::with_live_sources() builder
- control/lib.rs: re-export LiveSourceCapability
These provide the run-path capability signal the control plane threads to declare
which source kinds the engine can live-apply, ensuring X-Multiview-Apply headers
stay honest per build/run path (synthetic-only on software, synthetic+network on
ffmpeg).
BREAKING: bind_and_serve and related functions will need the live_sources param
in next commits; routes/sources.rs will call state.live_sources.is_live().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Changes live_apply_upsert to call state.live_sources.is_live(kind) instead of just checking is_synthetic(). This allows network/file kinds to be live-applied when the capability is set (full-pipeline run), and keeps ndi/youtube/aes67 always restart. Also updates the kind-change check to use is_live() for both synthetic->network and network->non-live transitions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…m ingest path Network/file kinds (rtsp/hls/ts/srt/rtmp/rist/file) now apply LIVE on the running full-pipeline engine through ONE uniform ingest path — the level-2 spine of the DEV-stack, re-authored against current main (the original PR #108 commits 9-way-conflict; pipeline.rs churned 49x since). - cli live_sources: SourceSpawn + IngestSpawner seam + SpawnedProducer; LiveSourceHub::start_with_ingest threads the run's spawner; the worker's SpawnSource arm tears down any prior producer (edit = store-reuse swap) then spawns through the seam; no spawner => held + warned (slate, inv #10). - cli pipeline: spawn_ingest_producer extracted — IngestSupervisor::start and the new LiveIngestSpawner call the SAME fn, so a runtime add runs exactly the startup supervised ingest_loop (reconnect/jitter/PTS-normalize/rw-timeout). Placement (ADR-W018 §7, gpu): drive_streaming publishes the pinned LiveIsland; select_live_decode_pick re-polls NVML and consults the SAME select_device scorer with the candidate set restricted to the island device (never frags/ migrates) — admit => island ordinal stamped, reject/island-vanished => that source decodes in software, warned. The output never falters (inv #1). - cli control: bind_and_serve takes the run's LiveSourceCapability; upsert_source routes a network kind to request_spawn_source (store create/reuse + route key + config mirror at the frame boundary, heavy spawn off-thread BEFORE the rebind — inv #1/#10); ndi/youtube/aes67 held with the reason. - cli main: ControlPlaneWiring carries the ingest spawner; the capability sent to bind_and_serve is DERIVED from ingest.is_some() — the header claims live for network kinds exactly when a real spawner backs it. Pipeline path wires Pipeline::live_ingest_spawner(); software path wires None. - control routes/sources: live_apply_upsert answers X-Multiview-Apply from the run capability; OpenAPI + doc updated. ADR-W018 amended for the as-built L2. Restores the e2e chroma pin PR #179 deferred (live_decode_chroma.rs): a live-added decode's U/V plane stats match the startup decode of the same clip. Gates (lane-local target, env -u CARGO_TARGET_DIR, -j1): fmt clean; clippy GREEN on default, ffmpeg, and ffmpeg+overlay+gpu; control typed_resources 23/23; 8 gpu placement unit tests incl the seam-pin spy; both ffmpeg e2e realtime proofs GREEN (live add reaches LIVE, remove slates to NO_SIGNAL, churn never falters). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…turation residual; refresh SourcesPage L2 copy
Pre-PR review close-outs (orchestrator-flagged):
- pipeline.rs: the `select_admission_pick` budget comment cited "ADR-0035 §5" for
the per-GPU perf-class CostBudget table being a future refinement — that
citation is WRONG (ADR-0035 has no §5 and does not scope the budget table).
Corrected to cite ADR-0018's hard-gate principle ("hard gates are the real
safety": VRAM headroom + capability + NVENC-session), and state the perf-class
table is future work (not yet built) — rule 27, no aspirational citation.
- pipeline.rs: `select_live_decode_pick` now DISCLOSES the known residual inline
— a VRAM-roomy but decode-engine-saturated GPU passes both the permissive
Mpix/s budget and the headroom gate, so a live add CAN be admitted onto a GPU
that cannot sustain another decode. Acceptable for this ship: the never-off-air
contract holds (an over-subscribed decode degrades the NEW tile, never the
program — inv #1/#2, the clock samples last-good) and inv #9's closed-loop
degradation sheds the cheapest tile if it bites. Comment-only; no code change.
- web SourcesPage: the apply-semantics callout + saved description said network/
file sources "go live via config export + restart" — now FALSE after L2 (they
apply live on a full-engine run). Refreshed the copy to match: synthetic +
network/file apply live on a full-engine run; NDI/YouTube/AES67 (and any kind
on the software engine) restart. The UI already reads the real
X-Multiview-Apply header — this is the explanatory copy catching up (rule 27).
Web gate (npm ci then): eslint --max-warnings=0 EXIT 0; tsc --noEmit EXIT 0;
lingui compile EXIT 0; vitest run 632 passed / 63 files EXIT 0 (incl the 2
SourcesPage suites 10/10). Rust: fmt clean; clippy -p multiview-cli
--features ffmpeg,overlay,gpu EXIT 0 (the comments compile clean).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ive decode forces software (2a67e4e) Carries the 2a67e4e amendment my initial re-author flattened away — a real overcommit/fragmentation bug, not a refinement. THE BUG (in my prior commit): select_live_decode_pick returned Option<String>, so a placement reject returned None, leaving IngestPlan.cuda_ordinal = None. In open_and_stream that means `new_preferring_hw(.., want_hw=true, None)` — NVDEC on libav's DEFAULT CUDA device. On a single-GPU over-headroom island the default device IS that island (overcommit); on a multi-GPU host it may be a DIFFERENT GPU (silent island fragmentation, forbidden by ADR-0018 never-fragment). "No ordinal" cannot distinguish *no placement decision* (default device fine) from *placement rejected* (hardware must NOT open). THE FIX: an explicit, closed tri-state DecodePlacement::{Default, Pinned(ordinal), SoftwareOnly} on IngestPlan + a single decoder_open_args gate: - SoftwareOnly -> (want_hw=false, None): hardware never attempted, even when NVDEC is compiled/present/not env-disabled. - Pinned(ordinal)/Default -> keep the canonical want_hw_decode env reading (MULTIVIEW_DISABLE_NVDEC opt-out still wins over a pin); Pinned threads the island ordinal. - select_live_decode_pick -> select_live_decode_placement: admit -> Pinned (or Default when the island resolved no ordinal); reject AND island-vanished -> SoftwareOnly, with truthful FORCING-software warnings. - drive_streaming stamps Pinned; ingest_plan_for defaults Default; open_and_stream routes through decoder_open_args. TDD: restored tests/decode_placement.rs (the f0a6b7f/2a67e4e4 RED pin) — it opens the REAL decoder via the gate's exact args and asserts a SoftwareOnly source comes back software (used_hw=false, no cuvid named); the gpu-lane unit tests now assert the placement value AND the decode-open behaviour per placement (not just the picked ordinal). ADR-W018 §7 amended for the as-built tri-state. Gate (env -u CARGO_TARGET_DIR, lane-local, -j1): fmt clean; clippy --workspace EXIT 0; clippy -p multiview-cli --features ffmpeg,overlay,gpu EXIT 0; decode_placement 2/2, gpu placement 8/8, live_network_apply + live_decode_chroma 1/1 each (ffmpeg). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er-claim X-Multiview-Apply:live (panel major) Codex panel (#181) MAJOR: RIST was advertised live-appliable unconditionally — `is_network_media()` includes `Self::Rist` and the binary derives `LiveSourceCapability::synthetic_and_network()` from `ingest.is_some()`. On an `ffmpeg`-WITHOUT-`rist` build the hub wires a real ingest spawner (so rtsp/hls/ts/srt/rtmp/file correctly flip to `live`), but a RIST source's `ingest_plan_for` arm is a typed refusal (`librist` not built) — so the header claimed `X-Multiview-Apply: live` for a spawn that refuses, and the tile rode the slate. The header over-claimed. Fix: `LiveSourceCapability` gains a separate `rist: bool` the binary sets from `cfg!(feature = "rist")` (via the new `with_rist` builder); `is_live` returns `self.rist` for `SourceKind::Rist` and `self.network` for the other network kinds. `synthetic_only` ⇒ rist:false; `synthetic_and_network` ⇒ rist:true (the canonical "full network" meaning), narrowed by the binary's `serve_control_plane` to the build's actual rist truth. RIST classification (`is_network_media`) is unchanged — RIST IS network media; only its *live-apply capability* is feature-gated, the honest level. Tests (control typed_resources, 23→25): `rist_stays_restart_when_the_engine_ build_lacks_the_rist_feature` (with_rist(false) ⇒ RIST restart + no UpsertSource, while a non-RIST network kind still flips live) and `rist_applies_live_when_the_ engine_build_carries_the_rist_feature` (with_rist(true) ⇒ RIST live + UpsertSource). Gate (env -u CARGO_TARGET_DIR, lane-local, -j1): fmt clean; clippy --workspace EXIT 0; clippy -p multiview-cli --features ffmpeg EXIT 0 AND --features ffmpeg,rist EXIT 0 (both cfg branches compile); control typed_resources 25/25. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…startup producers Codex panel round-2 on #181 found three real data-plane defects beyond the round-1 tri-state; all fixed here: - EMPTY-ISLAND fall-through (blocker): LiveIngestSpawner consulted select_live_decode_placement only when an island was published; an empty slot (startup admission rejected the GPU / no NVML) kept DecodePlacement::Default -> default-device NVDEC. decode_placement_for now forces SoftwareOnly when admission was attempted but named no island (fail closed, ADR-W018 §7). - ADMIT-with-no-ordinal (major): an island that passes budget but resolves no CUDA ordinal returned Default -> default-device NVDEC on the LIVE path (the compositor island is already pinned). Now forces SoftwareOnly. - TWO-WRITER race on a reused single-writer TileStore for a live EDIT of a STARTUP-origin source (blocker): teardown only joined hub-owned producers; a startup producer JoinHandle lives in IngestSupervisor, so the old decode thread could still write while the replacement published. register_stop now returns an exited latch every producer carries as an ExitGuard (flips on return or panic); teardown raises stop then bounded-waits on the latch (await_exits) before the replacement publishes -- covering startup-origin producers, not just hub-owned ones. ProducerStop.stop is pub(crate) so the cross-module registration tests raise it; the gpu-gated placement matches are if-let-else (clippy single_match_else). Gate (lane-local, env -u CARGO_TARGET_DIR, -j1): fmt + clippy --workspace + clippy -p multiview-cli --features ffmpeg,overlay,gpu + test -p multiview-cli --features ffmpeg + test --workspace all EXIT 0. Completes the stalled dev108-live-add lane WIP (round-2 authored there; the 5 ExitGuard call-site completions + the ProducerStop accessor + clippy/test fixups by the integrator). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… orphans the latch (round-2 panel) Codex round-2 panel on #181 flagged the ExitGuard-on-spawn-failure gap: register_stop inserts the stop entry + returns the exited latch BEFORE Builder::spawn, but the ExitGuard that flips exited was constructed INSIDE the closure — so a (rare) thread-spawn Err left an orphaned exited=false entry, and a later teardown of that id busy-waited the full TEARDOWN_JOIN_GRACE (3s) before detaching (a regression from the round-1 no-op join; bounded + off the output-clock thread, never an inv #1/#10 breach). Fix: construct the ExitGuard BEFORE the spawn and move it into the closure at all 7 producer-spawn sites (run.rs synth generator; pipeline.rs audio/tone/captions/youtube-reresolve + the spawn_ingest_producer decode thread; live_sources.rs hub synth). Its Drop now flips exited whether the thread runs (drops on exit) OR Builder::spawn fails (the closure owning the guard is dropped) — so a failed spawn deregisters cleanly and teardown never busy-waits an orphan. Gate (lane-local, env -u CARGO_TARGET_DIR, -j1): fmt + clippy --workspace + clippy -p multiview-cli --features ffmpeg,overlay,gpu + test -p multiview-cli --features ffmpeg + test --workspace all EXIT 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fault) on the live path + RIST live-apply is rist-feature-gated (round-3 panel) Two doc-drift minors the round-3 Codex panel flagged (rule 27 — the ADR prose contradicted the verified code): - §7 said an admitted live decode with no resolved CUDA ordinal stamps Default; the round-2 fix forces SoftwareOnly on the live path (Default is the startup placement, where nothing is pinned yet). Corrected. - The level-2 amendment said rist is included unconditionally; the code gates RIST live-applicability on cfg!(feature=rist) (with_rist), keeping an ffmpeg-without-rist build at restart. Corrected. Docs-only; the Rust build/clippy/tests are unaffected (green at 46ceea3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jun 19, 2026
aperim-agent
added a commit
that referenced
this pull request
Jun 19, 2026
) `transport::unified::tests::servicing_keeps_pace_with_the_receive_budget_under_flood` flooded a loopback transport for a fixed 400ms wall-clock window, then asserted `exhausted > 0` ("the flood must exhaust at least one receive budget"). On a loaded/contended CI runner the dedicated flooder OS thread is CPU-starved, so within 400ms it can exhaust ZERO budgets and the *precondition* assert fails — a runner-load-dependent timing assertion, not a real defect. It reddened the `feature-gated clippy (multiview-webrtc, native)` leg on main on at least two pushes (PR #181 era, the 2026-06-19 #195 merge), each forcing a manual re-run. Reproduced under CPU contention (2x nproc `sha256sum /dev/zero` hogs): ~1/30 runs fail, 100% of failures the `exhausted == 0` precondition at the old assert; the discriminating invariant never fails. Fix: drive the flood to a DETERMINISTIC number of exhausted budgets (`TARGET_EXHAUSTIONS = 8`) by polling `RunProbe::recv_budgets_exhausted` instead of sleeping a fixed window, then stop the driver. The flooder keeps the socket permanently readable, so the driver reaches the target given any CPU at all — the saturation precondition is now guaranteed regardless of runner load. A generous 30s `timeout` still bounds a wedged regression (it can never reach the target), so such a regression fails deterministically rather than hanging. The discriminating invariant is UNCHANGED and in fact strengthened: still `services >= exhausted` (servicing keeps pace with the receive budget under a genuinely saturating flood — invariant #10), now proven against >= 8 exhausted budgets (a sustained flood) rather than possibly a single trip. Test code only; no production behaviour change. Co-authored-by: verify <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ADR-W018 Level 2 — live add/edit of network sources (the DEV-stack spine)
Runtime-add a network ingest source to a running multiview — spawn the ingest producer, normalize/jitter it, place it into a tile — without disturbing the output. Network/file kinds (
rtsp/hls/ts/srt/rtmp/rist/file) now apply live on the running full-pipeline engine, through one uniform ingest path. This is the bottom of the DEV-stack and the API PR #179's deferred e2e chroma test needs.What it lands
Pipeline::live_ingest_spawner()+LiveSourceHub::start_with_ingest(...)— the production API: the hub gains anIngestSpawnerseam; the full-pipeline run wires the pipeline's spawner, the software run wiresNone.LiveSourceCapability { synthetic, network }on the controlAppState(+with_live_sources,is_live), re-exported frommultiview_control; the binary derives it fromingest.is_some()so theX-Multiview-Applyheader never over-claimslive.SourceKind::is_network_media()in the config schema — the level-2 classification point (disjoint fromis_synthetic;ndi/youtube/aes67in neither →restart).ristis included (it lowers to arist://AVIO URL and rides the identical ingest path).Re-authored against current main (not cherry-picked)
The original PR #108 commits (
23ca6955RED,afe6ca68impl) 9-way-conflict —pipeline.rschurned ~49× on main since. I read them as the intent, then re-implemented against current main's structures:LiveSourceHub,command_drain_with_live_sources,shared_stores, theupsert_sourcedrain arm that explicitly held the network path as "the next ADR-W018 slice"). This PR is that next slice.spawn_ingest_produceris extracted fromIngestSupervisor::startso the startup supervisor and the live hub call the same function — a runtime add runs exactly the startup supervisedingest_loop(same reconnect bracket, jitter, PTS-normalize, rw-timeout), never a second-quality copy. Main's loop had also grown a#[cfg(youtube)]re-resolve sibling since feat(cli,control,web): network-source live add — runtime ingest spawn with placement consult (ADR-W018 level 2) #108 — preserved, re-ordered to spawn before the extracted producer.LiveSourceCapabilitycoexists cleanly with main's laterLiveApplyCaps(overlays, ADR-W022) — two parallel per-collection capability signals onAppState.Invariant #1 (output-clock) — the live-added source is SAMPLED, never pacing
drive_streamingkeeps emitting one frame/tick regardless of the new ingest. The drain only does cheap binding mutations at the frame boundary (store create/reuse + route key + config mirror); the heavy half (libav open + thread spawn + placement poll) is handed to the off-threadLiveSourceHubover a bounded channel. The hub worker never touches the output clock. Soak proof:live_added_file_source_goes_live_then_remove_slates_never_faltering(ffmpeg) drives add → LIVE → remove →NO_SIGNAL→ rapid add/remove churn on a real pipeline and asserts!report.faltered.Invariant #10 (isolation) — one actor, bounded drop-oldest, no back-pressure
The ingest is one hub-owned producer thread feeding its per-source lock-free
TileStore(per-source isolation, no head-of-line blocking). The drain→hub channel is boundedtry_send(full ⇒ shed + warn, tile rides the slate); the control/add path cannot back-pressure the engine. Admission goes through the cost model:select_live_decode_pick(ADR-W018 §7) re-polls NVML and consults the sameselect_devicescorer with the candidate set restricted to the running island's device — a runtime add never fragments or migrates the GPU pipeline; an over-headroom/vanished island rejects to software decode, never overcommits. Chaos proof: the same churn test + the gpu placement unit tests (live_decode_pick_admits_onto_the_idle_island...,..._rejects_an_over_headroom_island_to_software,..._degrades_to_software_when_the_island_vanishes,live_spawner_consults_the_admission_path_on_every_decoded_spawn— exactly one fresh load poll per spawn).Restores PR #179's deferred e2e test
crates/multiview-cli/tests/live_decode_chroma.rs(deferred from #179 because it referenced the not-yet-builtlive_ingest_spawner/start_with_ingest) now compiles + runs + passes on the live-add path: a live-added decode's U/V plane statistics match the startup decode of the same clip (the Defect-A U/V-inversion pin).Gate evidence (lane-local target,
env -u CARGO_TARGET_DIR)cargo fmt --all -- --check— cleancargo clippy --workspace --all-targets -- -D warnings— exit 0cargo clippy -p multiview-cli --features ffmpeg --all-targets -- -D warnings— exit 0 (the ffmpeg-gated leg that caught fix(ffmpeg): bypass libswscale for identity NV12 scale (hw Defect A) #179's CI failure)cargo clippy -p multiview-cli --features ffmpeg,overlay,gpu --all-targets -- -D warnings— exit 0 (compiles the gpu-gated placement code)cargo test --workspace— exit 0cargo test -p multiview-cli --features ffmpeg(e2e) — both realtime proofs pass; gpu placement unit tests 8/8; controltyped_resources23/23 (incl. 5 new capability-matrix tests)cargo deny— not required (noCargo.toml/Cargo.lockchanges)HIGH-risk data-plane — not self-merging. Requesting the 3-lens cross-vendor panel (rule 21).
🤖 Generated with Claude Code
Pre-panel close-outs (orchestrator review items)
1. Inert cost-budget — verified + residual disclosed.
select_live_decode_pick(and the pre-existingselect_admission_pick) useCostBudget::new(100_000, 100_000, 100_000), an intentionally permissive per-engine Mpix/s budget. I verified the deferral claim: the budget being a near-inert gate with the VRAM-headroom hard gate + VRAM-dominant score as the real safety is documented in ADR-0018 ("hard gates are the real safety"; the cost model is "biased conservative"). NOTE: the pre-existing comment cited "ADR-0035 §5" for this — that citation is wrong (ADR-0035 has no §5 and does not scope a per-GPU perf-class budget table); commit 058b1fc corrects it to ADR-0018 and states the perf-class table is genuine future work (not yet built). Disclosed residual: a VRAM-roomy but decode-engine-saturated GPU passes both the permissive budget and the 0.85 headroom gate, so a live add CAN be admitted onto a GPU that cannot sustain another decode. Why acceptable for this ship: the never-off-air contract holds — an over-subscribed decode degrades the new tile only (the output clock samples last-good; the program is never touched — inv #1/#2), and invariant #9's closed-loop degradation sheds the cheapest-impact tile if the saturation bites. The reject→software-decode ladder (select_live_decode_pickreturnsNone⇒ software open) already covers VRAM/session/headroom exhaustion; only the decode-Mpix dimension is un-budgeted until the perf-class table lands.2. Management-completeness (§H) — confirmed, no new operator control needed (one copy fix). W018-L2 (live add/edit of a network source) is fully reachable through the existing source-CRUD UI:
web/src/pages/SourcesPage.tsxalready offers every network/file kind (rtsp/hls/ts/srt/rtmp/file/youtube) with typed locator + RTSP-transport fields, and surfacesX-Multiview-Applygenerically — so the server-side header flip needs zero new web wiring. The only web change required was honesty copy (rule 27): the apply-semantics callout + saved-description previously said network/file sources "go live via config export + restart", which my server change makes false. Commit 058b1fc refreshes that copy (synthetic + network/file apply live on a full-engine run; NDI/YouTube/AES67 and the software engine restart). No new API resource or UI control is missing.3. Full fresh gate re-run (lane-local target,
env -u CARGO_TARGET_DIR,-j1for Rust):cargo fmt --all -- --check→ exit 0cargo clippy --workspace --all-targets -- -D warnings→ exit 0cargo clippy -p multiview-cli --features ffmpeg --all-targets -- -D warnings→ exit 0cargo clippy -p multiview-cli --features ffmpeg,overlay,gpu --all-targets -- -D warnings→ exit 0cargo test --workspace→ exit 0 (a cold parallel build first hit the known spurious E0463can't find crate multiview_inputrace in an untouched crate; a warm-cache retry passed clean)cargo test -p multiview-cli --features ffmpeg→ the two e2e realtime proofs pass; gpu placement 8/8; control typed_resources 23/23web/changed):npm ci;eslint --max-warnings=0exit 0;tsc --noEmitexit 0;lingui compileexit 0;vitest run632 passed / 63 files exit 0cargo deny— not required (noCargo.toml/Cargo.lockchanges)CI attribution: on the prior CI run both
check + testjobs (macos + ubuntu) passed. The two red jobs were NOT attributable to this PR:web (eslint+tsc+vitest+build)fails onmainat the same base SHA (pre-existing — locally the full web suite passes 632/632 here, so it is a CI-env flake in the WhepPlayer media tests), andfeature-gated clippy (multiview-webrtc, native)is a flaky timing test (servicing_keeps_pace_with_the_receive_budget_under_flood) in an untouched crate that passed on re-run.Correctness fix (commit 943706a) — DecodePlacement tri-state
Orchestrator review caught that my initial re-author flattened the
2a67e4e4DecodePlacement amendment intoOption<String>— a real overcommit/fragmentation bug, now fixed.The bug:
select_live_decode_pickreturnedOption<String>, so a placement reject returnedNone, leavingIngestPlan.cuda_ordinal = None. Inopen_and_streamthat becomesnew_preferring_hw(.., want_hw=true, None)→ NVDEC opens on libav's default CUDA device. On a single-GPU over-headroom island the default device IS that island (overcommit); on a multi-GPU host it may be a different GPU (silent island fragmentation, forbidden by ADR-0018). "No ordinal" cannot distinguish no placement decision (default device fine) from placement rejected (hardware must NOT open).The fix: an explicit, closed tri-state
DecodePlacement::{Default, Pinned(ordinal), SoftwareOnly}onIngestPlan+ a singledecoder_open_argsgate:SoftwareOnly → (want_hw=false, None)forces software even when NVDEC is compiled/present/not env-disabled;Pinned/Defaultkeep the canonicalwant_hw_decodeenv reading (operatorMULTIVIEW_DISABLE_NVDECstill wins over a pin).select_live_decode_pick → select_live_decode_placement: admit →Pinned(orDefaultwhen the island resolved no ordinal); reject AND island-vanished →SoftwareOnly. Restoredtests/decode_placement.rs(the f0a6b7f/2a67e4e4 RED pin), which opens the real decoder via the gate's exact args and asserts aSoftwareOnlysource comes back software (used_hw=false, no cuvid named); the gpu-lane unit tests now assert the placement value AND the decode-open behaviour per placement, not just the picked ordinal. ADR-W018 §7 amended for the as-built tri-state.Re-gated after the fix (all green, lane-local target,
env -u CARGO_TARGET_DIR,-j1for Rust):cargo fmt --all -- --checkexit 0;cargo clippy --workspace --all-targets -- -D warningsexit 0;cargo clippy -p multiview-cli --features ffmpeg,overlay,gpu --all-targets -- -D warningsexit 0;cargo test --workspaceexit 0;cargo test -p multiview-cli --features ffmpeg,overlay,gpu:decode_placement2/2, gpu placement units 8/8,live_network_apply+live_decode_chroma1/1 each.Panel-blocker resolutions (head 1abed23)
The Codex 3-lens panel reviewed head
058b1fcb(before the tri-state fix) and returned two findings; both are now resolved.Blocker #1 (tri-state to the open path) — fixed in 943706a (the "DecodePlacement tri-state" section above). The panel saw the old
Option<String>code where a reject'sNoneopened NVDEC on the default device. NowDecodePlacement::{Default,Pinned,SoftwareOnly}threads throughIngestPlanto the open path,SoftwareOnly → decoder_open_args → (want_hw=false, None), andtests/decode_placement.rsasserts SOFTWARE behaviour at the open path (the realnew_preferring_hwcall →used_hw=false, no cuvid named), not just that the pick returnsNone.Major #2 (RIST capability gating) — fixed in 1abed23. RIST was advertised live-appliable unconditionally, so an
ffmpeg-without-ristbuild over-claimedX-Multiview-Apply: livefor a RIST source whose spawn would refuse (tile rides slate). Fixed:LiveSourceCapabilitygains arist: boolthe binary sets fromcfg!(feature = "rist")(viawith_rist);is_livereturnsself.ristforSourceKind::Rist,self.networkfor the other network kinds.is_network_mediaclassification is unchanged (RIST is network media); only its live-apply capability is feature-gated — the honest level. Two newtyped_resourcestests pin both directions (with_rist(false)⇒ RISTrestart+ noUpsertSourcewhile a non-RIST network kind still flipslive;with_rist(true)⇒ RISTlive+UpsertSource).Re-gated after both fixes (all green, lane-local target,
env -u CARGO_TARGET_DIR,-j1for Rust):cargo fmt --all -- --checkexit 0;cargo clippy --workspace --all-targets -- -D warningsexit 0;cargo clippy -p multiview-cli --features ffmpegexit 0 AND--features ffmpeg,ristexit 0 AND--features ffmpeg,overlay,gpuexit 0 (all cfg branches compile);cargo test --workspaceexit 0; controltyped_resources25/25;decode_placement2/2; gpu placement units 8/8;live_network_apply+live_decode_chroma1/1 each.