Repository navigation
Conversation
… start wipe_between_letterboxed_sources_animates pulled PGM from an appsink with max-buffers=1 drop=true, so it only ever saw the newest frame. On a slow software-GL runner the mixer can render the whole mid-wipe stretch while one frame is scanned, and a wipe that animated reads as a hard switch. The appsink now holds four frames and pushes back instead of dropping, so the observer sees every frame. Every time in the wipe is stream time, so a mixer held behind the wall clock renders the same frames. The wall-clock settle sleep is replaced by waiting for the white picture and both sources' stored sizes, and the failure message now carries the observed frames. source_resized_during_a_fade_is_fitted_after_it failed in Running::start with a state change *failure* (not a timeout): an element posted an error during PAUSED -> PLAYING. The tests run without a log subscriber, so the error was lost. The panic now lists the errors broadcast by the bus watch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
srperens
left a comment
There was a problem hiding this comment.
Verdict: Approve. I am posting this as a COMMENT review because GitHub refuses APPROVE on a PR from the account this stage runs under. The change is test-only and every mechanism claim holds in code. The two changed binaries executed on llvmpipe and passed. Merge order against #935/#936 is mechanical, see Overlaps.
Claims
| Claim | Verdict | Evidence |
|---|---|---|
| The observer now sees every PGM frame | CONFIRMED |
backend/tests/vision_mixer_fx_test.rs:324 — ("drop", PV::Bool(false)), (main had drop=true, max-buffers=1) |
| Holding the mixer back keeps the wipe consistent in stream time | CONFIRMED |
The take's now is the mixer's own output position, not the wall clock: backend/src/gst/transitions/controller.rs:73 — mixer_position.ok_or(TransitionError::PositionQueryFailed). The shader's u_start latches from branch PTS (program_wipe_at_first_buffer) |
| The settle sleep can be replaced by a wait on input sizes | CONFIRMED |
The take reads aspects synchronously, with no idle callback (this test runs no glib main loop): backend/src/gst/pipeline/effects/take.rs:145 — self.vision_mixer_source_aspects(block_instance_id, state.num_inputs); |
| A failed start now names the posted error | CONFIRMED |
The subscription exists before start(): backend/tests/vision_mixer_source_resize_test.rs:248 — let mut posted = events.subscribe();. The main loop that dispatches the bus signal watch is already running (below) |
| The CI wipe flake was the observer skipping frames | EXTERNAL |
This cannot be settled from the repo, and the body says so. The assumption is that a recurrence will now carry a per-frame PTS trace, which separates "observer skipped" from "product hard-switched" |
The main loop starts first: backend/tests/vision_mixer_source_resize_test.rs:243 — std::thread::spawn(move || ml.run())
Diagnosis. The old max-buffers=1 drop=true observer could only report a hard switch if it fell behind for the whole mid-wipe stretch. That fits the fixed end white=0.00 red=0.76 signature. Fix coverage is BOUNDED: it covers the wipe flake only. The resize start failure is made diagnosable, not fixed, and the body states that honestly. The hard-switch guard survives because saw_both never sets, and the observer stops at 300 frames or the 30 s deadline (backend/tests/vision_mixer_fx_test.rs:481 — if trace.len() > 300 {). The author's 1 ms forced-wipe check shows the assert still fires.
Radius. LOCAL: two test files, no product code. Worst-case runtime on failure rises to about 90 s (three 30 s deadlines). Nothing on the passing path.
Overlaps
- #935 (and #936, which carries the same hunks) replaces the hand-built
PipelineManager::newin both files withcommon::manager::build. This is a textual conflict only. #935 also addsbuild_with(flow, events, media_path), so whichever PR lands second keepsevents.subscribe()by building throughbuild_with. No design decision is involved. - #1007 changes
shader_fx.rs, which this wipe test guards. Landing #994 first means #1007's CI runs against the stricter observer. Preferable, not required.
Tests & CI. At fff843b, all runs are green: Check (Linux), Build (Linux x86_64/ARM64), Check & Build (WASM), API Contract Check, Static JS Tests. macOS and Windows were skipping, which is acceptable for test-only Rust that the author ran on macOS. In the Check (Linux) log, vision_mixer_fx_test passed 2 tests in 3.74 s and vision_mixer_source_resize_test passed 8 in 8.80 s. Real GL runs, not millisecond skips (STROM_REQUIRE_GL=1 is set in ci.yml). One green run cannot show the flake is gone. The per-frame gst_mini_object_unref CRITICAL also appears in this job and is pre-existing, as the body notes.
Confidence: MED
|
Closing.
🤖 Generated with Claude Code |
Refs #977
Problem
Since #977 put the GPU vision mixer tests on Linux CI (Mesa llvmpipe, surfaceless EGL,
STROM_REQUIRE_GL=1), two of them fail now and then on the Linux runner. They pass on macOS.vision_mixer_fx_test::wipe_between_letterboxed_sources_animatesfailed 8 times on 2026-10-05, for example runs 37306914593, 37304562884, 37301781398 and 37324119848. It fails the same way every time, always in the first (classic) wipe:classic wipe: animated=false end white=0.00 red=0.76.vision_mixer_source_resize_test::source_resized_during_a_fade_is_fitted_after_itfailed once (run 37304745163) withstart GPU vision mixer pipeline: StateChange("State change failed: StateChangeError - current: Paused, pending: Playing").Change
Wipe test: the observer now sees every PGM frame. The test pulled PGM from an appsink with
max-buffers=1 drop=true, so it only ever saw the newest frame. If the observer falls behind for the length of the mid-wipe stretch (about 1.4 s of the 2 s wipe has both sources on screen), it jumps from the last white frame to the first red one. It then reports a hard switch, the exact signature above. The appsink now holds 4 frames and then pushes back (drop=false).Holding the mixer back does not change what it renders. The pad keyframes, the shader's latched
u_startand the PGM frames are all in stream time, and the live sources keep counting timestamps, so a mixer running behind the wall clock renders the same frames. Two things follow from that:vision_mixer_input_resolutionsreporting both sources' sizes. That is what the sleep was for: the take aspect-fits the pads from those sizes.When the test fails, the message now lists every frame it saw (PTS, white and red fractions). If CI fails again, the trace shows whether the PTS really jump from the before-wipe frame to the after-wipe frame (a product problem), or whether there are frames in between.
Resize test: a failed start now names the error.
PipelineManager::startreported a state-change failure, not a timeout.get_statereturnsErronly forGST_STATE_CHANGE_FAILURE, which means an element posted an error during PAUSED to PLAYING. The bus watch logs that error witherror!and broadcasts it asStromEvent::PipelineError. The tests run without a log subscriber, though, so the error was lost.Running::startnow subscribes to the flow's events and puts the posted errors in the panic message. No test-side wait could fix this one: the start really failed. I did not retry the start, or serialise the 8 concurrent GL pipelines, because either would hide what may be a real "GPU mixer fails to start under load" bug.Rejected:
latency. It changes the setup under test, and nothing showed lateness. Injecting 0.3 to 2.5 s of branch lag locally made the wipe choppy, but it still animated.#[ignore]or skipping on llvmpipe.Evidence
shader_wipe_taketo a 1 ms duration, and the test failed withclassic wipe should animate (no frame showed both sources), frames: ["0:00:00.066666666: w=0.74 r=0.00", "0:00:00.100000000: w=0.00 r=0.76", "0:00:00.133333333: w=0.00 r=0.76", .... That is a clean cut between two consecutive frames, the same end state as on CI.inverted wipe should animate (no frame showed both sources), frames: ["0:00:01.633333333: w=0.00 r=0.76", "0:00:01.666666666: w=0.74 r=0.00", ....filesrcwith a missing file into the flow, and it panicked withstart GPU vision mixer pipeline: StateChange("Failed to start: Element failed to change its state"); errors posted: ["badsrc: Resource not found.", ...]. I removed that test afterwards.Tests
Ran, macOS (native GL, Apple Silicon):
STROM_REQUIRE_GL=1 STROM_REQUIRE_GST_PLUGINS=1 cargo test --test vision_mixer_fx_test: 3 runs, 2/2 passed each time.STROM_REQUIRE_GL=1 STROM_REQUIRE_GST_PLUGINS=1 cargo test --test vision_mixer_source_resize_test: 3 runs, 8/8 passed each time.cargo fmt,cargo clippy --all-targets: clean.Skipped: none (GL was required).
Not run: llvmpipe / Linux, see Evidence.
For the reviewer
Out of scope
GStreamer-CRITICAL: gst_mini_object_unref: assertion 'mini_object != NULL' failedthat the GL tests print on the Linux runner, in passing runs too. It is worth its own look.max-buffers=1 drop=true. The fade test compares consecutive frames (largest_step < 40), so it has the same exposure to skipped frames, but it has not failed so far.WIPE_ALPHA_LEAD_MScovers about 2 frames in flight). I saw it while injecting lag locally. It is a product nit, not this flake.🤖 Generated with Claude Code