From f46421da72ad2f6ab389f2770bb17ade92c41e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E8=B6=85=E8=B6=85=E8=B6=85=E8=B6=85=E7=BA=A7?= =?UTF-8?q?=E5=96=9C=E6=AC=A2=E4=BD=A0=E7=9A=84=E8=BE=BE=E5=A6=AE=E5=A8=85?= <176143450+My-Denia@users.noreply.github.com> Date: Wed, 11 Nov 2026 13:09:08 +0800 Subject: [PATCH 1/3] test(compositor): check the held end frame by its picture, not the clock The clock now runs on to the clip's end while the last frame is held, and the published position reports that clock, so `held < end` can no longer pass. Check the held frame's picture instead: the fixture turns lime at 0.4 s, so a 0.4 s cut must hold a blue frame and the 0.6 s clip a lime one. CI only runs `cargo check` on Windows, so this runtime test never ran there. --- crates/compositor/src/pipeline_windows.rs | 51 ++++++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/crates/compositor/src/pipeline_windows.rs b/crates/compositor/src/pipeline_windows.rs index 4c59cf73c..3e95ed604 100644 --- a/crates/compositor/src/pipeline_windows.rs +++ b/crates/compositor/src/pipeline_windows.rs @@ -2274,6 +2274,20 @@ mod tests { out } + /// A 64x64 clip that is blue before `change_sec` and lime from there on. + fn encode_blue_then_green( + codec_args: &[&str], + filename: &str, + change_sec: &str, + duration_sec: &str, + ) -> std::path::PathBuf { + let filter = + format!("drawbox=x=0:y=0:w=iw:h=ih:color=lime:t=fill:enable='gte(t,{change_sec})'"); + let mut args = vec!["-vf", filter.as_str()]; + args.extend_from_slice(codec_args); + encode_named_color(&args, filename, "blue", duration_sec) + } + unsafe fn allocated_decoder_open_resources(filename: &str) -> DecoderOpenResources { let path = encode_color(&["-c:v", "libopenh264", "-b:v", "200k"], filename); let cpath = CString::new(path.to_string_lossy().as_bytes()).expect("fixture path CString"); @@ -3074,7 +3088,9 @@ mod tests { let h264 = ["-c:v", "libopenh264", "-b:v", "200k"]; // Long enough that a view which wrapped onto it is still there when the test looks. let first = encode_named_color(&h264, "997-first.mp4", "red", "3.0"); - let last = encode_named_color(&h264, "997-last.mp4", "blue", "0.6"); + // Blue up to 0.4 s, green from the frame at exactly 0.4 s: which colour is held at the end + // tells the clip's own last frame from the first frame its 0.4 s cut removed. + let last = encode_blue_then_green(&h264, "997-last.mp4", "0.4", "0.6"); // The paths travel in JSON, and the view matches them against its requests as strings. let first = first.to_str().expect("utf8").replace('\\', "/"); let last = last.to_str().expect("utf8").replace('\\', "/"); @@ -3105,6 +3121,8 @@ mod tests { let first_frame = wait_for_frame(&view, 0, |p| p.clip_index == 2); let mut gen = first_frame.0; let mut held = first_frame.5; + let mut held_rgba = first_frame.3; + let (mut held_w, mut held_h) = (first_frame.1, first_frame.2); // Play to the end of the last clip, then about as long again. view.set_playing(true); @@ -3114,15 +3132,36 @@ mod tests { while let Some(frame) = view.latest_frame_since(gen) { gen = frame.0; held = frame.5; + held_rgba = frame.3; + (held_w, held_h) = (frame.1, frame.2); } - // The frame held at the end is the clip's own last frame, not the first one its cut - // removed (the 25 fps file has a frame at exactly 0.4 s). + // Holding at the end leaves the published time (the clock) on the clip's end, so it + // can equal `last_end_sec` but never pass it. assert!( - held.source_time_sec < last_end_sec, - "end {last_end_sec}: held a frame past the clip's end, at {:.3} s", + held.source_time_sec <= last_end_sec + 1e-6, + "end {last_end_sec}: the clock ran past the clip's end, at {:.3} s", held.source_time_sec ); - // And it did play up to there: a view stalled at its start would pass the bound above. + // The held frame must not be the first one the 0.4 s cut removed (the 25 fps file has + // a frame at exactly 0.4 s, which is green). The clock cannot tell them apart, so + // judge by the picture. For 0.6, green only shows the frame is past the 0.4 s change, + // not that it is the file's very last frame (0.44 is green too); the lower clock + // bound below is what shows playback reached the end. + let center = (((held_h / 2) * held_w + held_w / 2) * 4) as usize; + let rgb = &held_rgba[center..center + 3]; + let (r, g, b) = (i32::from(rgb[0]), i32::from(rgb[1]), i32::from(rgb[2])); + if last_end_sec < 0.5 { + assert!( + b - r > 100 && b - g > 100, + "end {last_end_sec}: expected the blue frame before the cut, got rgb {rgb:?}" + ); + } else { + assert!( + g - r > 100 && g - b > 100, + "end {last_end_sec}: expected a frame past the 0.4 s change (green): {rgb:?}" + ); + } + // And it did play up to there: a view stalled at its start would pass the bounds above. assert!( held.source_time_sec > last_end_sec - 0.1, "end {last_end_sec}: playback did not reach the clip's end, held at {:.3} s", From c821878472f95d24291407e7eb9eca0421718375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E8=B6=85=E8=B6=85=E8=B6=85=E8=B6=85=E7=BA=A7?= =?UTF-8?q?=E5=96=9C=E6=AC=A2=E4=BD=A0=E7=9A=84=E8=BE=BE=E5=A6=AE=E5=A8=85?= <176143450+My-Denia@users.noreply.github.com> Date: Wed, 11 Nov 2026 21:47:21 +0800 Subject: [PATCH 2/3] test(compositor): give each end's last frame its own colour The published time is the hold clock, which runs on to the clip's end while a frame is held, so a clock bound passes even if decoding stopped early. Colour the last frame each end keeps (magenta before the 0.4 s cut, yellow for the file's last frame) and assert that colour instead. The lower clock bound is dropped: a stalled view now shows blue or lime. --- crates/compositor/src/pipeline_windows.rs | 56 +++++++++++------------ 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/crates/compositor/src/pipeline_windows.rs b/crates/compositor/src/pipeline_windows.rs index 3e95ed604..dbfd19104 100644 --- a/crates/compositor/src/pipeline_windows.rs +++ b/crates/compositor/src/pipeline_windows.rs @@ -2274,18 +2274,23 @@ mod tests { out } - /// A 64x64 clip that is blue before `change_sec` and lime from there on. - fn encode_blue_then_green( - codec_args: &[&str], - filename: &str, - change_sec: &str, - duration_sec: &str, - ) -> std::path::PathBuf { - let filter = - format!("drawbox=x=0:y=0:w=iw:h=ih:color=lime:t=fill:enable='gte(t,{change_sec})'"); + /// A 64x64, 25 fps, 0.6 s clip whose frames around the two cut points get their own colour: + /// blue before 0.36 s, magenta 0.36-0.399 s (the last frame a 0.4 s cut keeps), lime from + /// 0.4 s, yellow from 0.56 s (the file's last frame). + fn encode_end_frame_colours(codec_args: &[&str], filename: &str) -> std::path::PathBuf { + let boxes = [ + ("magenta", "between(t,0.36,0.399)"), + ("lime", "between(t,0.4,0.559)"), + ("yellow", "gte(t,0.56)"), + ]; + let filter = boxes + .iter() + .map(|(c, en)| format!("drawbox=x=0:y=0:w=iw:h=ih:color={c}:t=fill:enable='{en}'")) + .collect::>() + .join(","); let mut args = vec!["-vf", filter.as_str()]; args.extend_from_slice(codec_args); - encode_named_color(&args, filename, "blue", duration_sec) + encode_named_color(&args, filename, "blue", "0.6") } unsafe fn allocated_decoder_open_resources(filename: &str) -> DecoderOpenResources { @@ -3088,9 +3093,9 @@ mod tests { let h264 = ["-c:v", "libopenh264", "-b:v", "200k"]; // Long enough that a view which wrapped onto it is still there when the test looks. let first = encode_named_color(&h264, "997-first.mp4", "red", "3.0"); - // Blue up to 0.4 s, green from the frame at exactly 0.4 s: which colour is held at the end - // tells the clip's own last frame from the first frame its 0.4 s cut removed. - let last = encode_blue_then_green(&h264, "997-last.mp4", "0.4", "0.6"); + // The last frame before each end has its own colour (magenta before the 0.4 s cut, yellow + // for the file's last frame), so the held picture says which frame the view stopped on. + let last = encode_end_frame_colours(&h264, "997-last.mp4"); // The paths travel in JSON, and the view matches them against its requests as strings. let first = first.to_str().expect("utf8").replace('\\', "/"); let last = last.to_str().expect("utf8").replace('\\', "/"); @@ -3135,38 +3140,29 @@ mod tests { held_rgba = frame.3; (held_w, held_h) = (frame.1, frame.2); } - // Holding at the end leaves the published time (the clock) on the clip's end, so it - // can equal `last_end_sec` but never pass it. + // The published time is the hold clock: it runs on to the clip's end even if decoding + // stopped early, so it can only bound the clock (equal to the end, never past it). assert!( held.source_time_sec <= last_end_sec + 1e-6, "end {last_end_sec}: the clock ran past the clip's end, at {:.3} s", held.source_time_sec ); - // The held frame must not be the first one the 0.4 s cut removed (the 25 fps file has - // a frame at exactly 0.4 s, which is green). The clock cannot tell them apart, so - // judge by the picture. For 0.6, green only shows the frame is past the 0.4 s change, - // not that it is the file's very last frame (0.44 is green too); the lower clock - // bound below is what shows playback reached the end. + // The colour proves which frame is held: the last one the cut keeps (magenta), or the + // file's last frame (yellow). Stuck on an earlier frame shows blue or lime instead. let center = (((held_h / 2) * held_w + held_w / 2) * 4) as usize; let rgb = &held_rgba[center..center + 3]; let (r, g, b) = (i32::from(rgb[0]), i32::from(rgb[1]), i32::from(rgb[2])); if last_end_sec < 0.5 { assert!( - b - r > 100 && b - g > 100, - "end {last_end_sec}: expected the blue frame before the cut, got rgb {rgb:?}" + r > 150 && b > 150 && g < 100, + "end {last_end_sec}: expected magenta, the last frame the cut keeps: {rgb:?}" ); } else { assert!( - g - r > 100 && g - b > 100, - "end {last_end_sec}: expected a frame past the 0.4 s change (green): {rgb:?}" + r > 150 && g > 150 && b < 100, + "end {last_end_sec}: expected yellow, the file's last frame: {rgb:?}" ); } - // And it did play up to there: a view stalled at its start would pass the bounds above. - assert!( - held.source_time_sec > last_end_sec - 0.1, - "end {last_end_sec}: playback did not reach the clip's end, held at {:.3} s", - held.source_time_sec - ); // A seek inside the last clip, as the app sends it while paused. view.set_time(0.2); From 2180fc2c6f345392b041194986808790788571ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E8=B6=85=E8=B6=85=E8=B6=85=E8=B6=85=E7=BA=A7?= =?UTF-8?q?=E5=96=9C=E6=AC=A2=E4=BD=A0=E7=9A=84=E8=BE=BE=E5=A6=AE=E5=A8=85?= <176143450+My-Denia@users.noreply.github.com> Date: Wed, 11 Nov 2026 22:30:12 +0800 Subject: [PATCH 3/3] test(compositor): bound the held clock from below and check the held clip The colour proves the held frame, but a clock stuck at 0 would pass without a lower bound. Assert the held clip index before the seek too, since clips 1 and 2 can end on the same yellow frame. --- crates/compositor/src/pipeline_windows.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/compositor/src/pipeline_windows.rs b/crates/compositor/src/pipeline_windows.rs index dbfd19104..3a0fa0492 100644 --- a/crates/compositor/src/pipeline_windows.rs +++ b/crates/compositor/src/pipeline_windows.rs @@ -3141,12 +3141,18 @@ mod tests { (held_w, held_h) = (frame.1, frame.2); } // The published time is the hold clock: it runs on to the clip's end even if decoding - // stopped early, so it can only bound the clock (equal to the end, never past it). + // stopped early. The bounds check the clock (reached the end, never past it); the + // colour below checks which frame is held. assert!( held.source_time_sec <= last_end_sec + 1e-6, "end {last_end_sec}: the clock ran past the clip's end, at {:.3} s", held.source_time_sec ); + assert!( + held.source_time_sec > last_end_sec - 0.1, + "end {last_end_sec}: the clock did not reach the clip's end, held at {:.3} s", + held.source_time_sec + ); // The colour proves which frame is held: the last one the cut keeps (magenta), or the // file's last frame (yellow). Stuck on an earlier frame shows blue or lime instead. let center = (((held_h / 2) * held_w + held_w / 2) * 4) as usize; @@ -3164,6 +3170,12 @@ mod tests { ); } + assert_eq!( + held.clip_index, 2, + "end {last_end_sec}: held a frame from clip {} instead of the last clip", + held.clip_index + ); + // A seek inside the last clip, as the app sends it while paused. view.set_time(0.2); let (_, w, h, rgba, _, position) =