app: refresh the paused frame after the debounced snapshot upload
Edits invalidated the display caches, but the debounced snapshot upload (150 ms) landed later - meanwhile the viewer had already rendered and cached a proxy through the stale snapshot, and nothing invalidated it again, so a paused monitor kept the pre-edit picture until the playhead moved. Re-invalidate when the fresh snapshot is actually installed so the next paint re-renders from it.
This commit is contained in:
@@ -3118,7 +3118,16 @@ impl RealEngine {
|
||||
return;
|
||||
};
|
||||
if let Some(m) = RenderManager::global() {
|
||||
let _ = m.set_graph_snapshot(&project, revision);
|
||||
if m.set_graph_snapshot(&project, revision).is_ok() {
|
||||
// Every frame rendered since the edit went through workers
|
||||
// holding the STALE snapshot (the debounce window — the
|
||||
// proxy that just landed in the cache, a full-res fill,
|
||||
// the pre-render window). Drop them so the next paint
|
||||
// re-renders from the fresh snapshot; otherwise a paused
|
||||
// viewer keeps the pre-edit picture until the playhead
|
||||
// moves (the "edits only show during playback" report).
|
||||
self.invalidate_rendered_frames();
|
||||
}
|
||||
}
|
||||
self.last_snapshot_push = Some(std::time::Instant::now());
|
||||
cx.notify();
|
||||
@@ -8809,6 +8818,110 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// An effect edit must reach the PAUSED display: the debounced
|
||||
/// snapshot upload invalidates the frames rendered through the stale
|
||||
/// snapshot, so a bounded number of engine ticks after the edit the
|
||||
/// program frame shows the new picture — without any playback
|
||||
/// (the "edits only show during playback" report's regression).
|
||||
#[gpui::test]
|
||||
async fn effect_edit_updates_the_paused_frame(cx: &mut gpui::TestAppContext) {
|
||||
let _media = media_lock();
|
||||
let _worker = WorkerBinGuard::set();
|
||||
let engine = cx.update(|cx| cx.new(|cx| RealEngine::create(cx)));
|
||||
cx.update(|app| engine.update(app, |engine, cx| engine.new_project(cx)));
|
||||
|
||||
// A solid green clip on the timeline, selected for the stack.
|
||||
let media = std::env::temp_dir()
|
||||
.join(format!("oakapp_editframe_{}.mp4", std::process::id()));
|
||||
cx.update(|app| {
|
||||
engine.update(app, |engine, _cx| {
|
||||
let project = engine.project.clone().unwrap();
|
||||
let seq = engine.sequence.unwrap();
|
||||
oak_codec::testmedia::write_test_clip_solid(
|
||||
&media,
|
||||
64,
|
||||
64,
|
||||
10,
|
||||
10,
|
||||
[0.0, 1.0, 0.0, 1.0],
|
||||
)
|
||||
.expect("green clip");
|
||||
let footage = graphops::import_footage(&project, &media).expect("import");
|
||||
graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the clip");
|
||||
engine.refresh_sequence_info();
|
||||
engine.rebuild_timeline();
|
||||
let clip = engine
|
||||
.tracks
|
||||
.iter()
|
||||
.flat_map(|t| t.clips.iter())
|
||||
.next()
|
||||
.expect("a clip")
|
||||
.id();
|
||||
engine.selected_clip = Some(clip);
|
||||
})
|
||||
});
|
||||
|
||||
let frame_bytes = |cx: &mut gpui::TestAppContext| -> Vec<u8> {
|
||||
cx.read(|app| {
|
||||
engine
|
||||
.read(app)
|
||||
.cpu_frame(Monitor::Program, app)
|
||||
.as_bytes(0)
|
||||
.expect("frame bytes")
|
||||
.to_vec()
|
||||
})
|
||||
};
|
||||
let before = frame_bytes(cx);
|
||||
|
||||
// Insert an opacity effect and crush it to 0 (the frame goes
|
||||
// transparent black) — through the same engine paths the
|
||||
// inspector's add / parameter commit take (the stack appends at
|
||||
// the chain end, like the panel's add button).
|
||||
cx.update(|app| {
|
||||
engine.update(app, |engine, cx| {
|
||||
engine
|
||||
.add_effect(usize::MAX, "org.olivevideoeditor.Olive.opacity", cx)
|
||||
.expect("add opacity");
|
||||
let effect = engine
|
||||
.selected_effect_cards()
|
||||
.last()
|
||||
.expect("the opacity card (closest to the clip)")
|
||||
.id();
|
||||
engine
|
||||
.set_effect_param(
|
||||
EffectId(effect.0),
|
||||
"opacity_in",
|
||||
oak_node::value::NodeValue::Float(0.0),
|
||||
cx,
|
||||
)
|
||||
.expect("set opacity 0");
|
||||
})
|
||||
});
|
||||
|
||||
// The paused picture must catch up without any playback: after the
|
||||
// edit the display caches are invalidated and the debounced
|
||||
// snapshot upload invalidates whatever was rendered through the
|
||||
// stale snapshot, so a bounded number of engine ticks later the
|
||||
// program frame shows the new picture.
|
||||
let mut pumps = 0usize;
|
||||
loop {
|
||||
pumps += 1;
|
||||
cx.update(|app| engine.update(app, |engine, cx| engine.tick(cx)));
|
||||
if frame_bytes(cx) != before {
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
pumps < 5000,
|
||||
"the paused frame never picked up the effect edit (still the pre-edit pixels after {pumps} ticks)"
|
||||
);
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
oak_undo::global::clear().unwrap();
|
||||
let _ = std::fs::remove_file(&media);
|
||||
}
|
||||
|
||||
/// A fresh sequence starts with the default 2 video + 2 audio track
|
||||
/// layout (user-mandated: V1, V2 on top, A1, A2 below) — and the
|
||||
/// default layout is NOT an undoable edit (the undo stack stays
|
||||
|
||||
Reference in New Issue
Block a user