mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-12 21:32:52 +00:00
#3493: drop the viewer's frames once a LUT file in use changes
Renders now follow a LUT file, but the viewer and RAM preview kept the frames already rendered with the old one. A small worker stamps the LUT files in use every two seconds and wakes the interface when one changed; the interface then drops its frames, and a frame whose render started before the change is not kept when it finishes.
This commit is contained in:
8 files changed
+144
-1
No files matched your search
@@ -1,6 +1,7 @@
|
||||
pub mod doc_path;
|
||||
pub mod footage_check;
|
||||
pub mod footage_reload;
|
||||
pub mod lut_reload;
|
||||
pub mod media_browser;
|
||||
pub mod media_cache;
|
||||
pub mod sequence;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//! LUT files that change on disk (#3493).
|
||||
//!
|
||||
//! A render follows a LUT file by itself: [`effectcraft_effects::load_lut`] stamps the file and
|
||||
//! reads it again once it changed, and a layer's cache key holds the stamp. What the desktop app
|
||||
//! keeps of earlier renders, the frames of its viewer and RAM preview, is dropped when
|
||||
//! [`effectcraft_effects::lut_generation`] moves. The [`Watcher`] looks at the LUT files in use
|
||||
//! every two seconds, so an edit shows without anything rendering first, and wakes the interface
|
||||
//! only when one changed.
|
||||
|
||||
use std::sync::mpsc::{RecvTimeoutError, SyncSender};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Moves each time a LUT file in use was found changed on disk: frames rendered before may show
|
||||
/// the old LUT ([`effectcraft_effects::lut_generation`]).
|
||||
pub fn generation() -> u64 {
|
||||
effectcraft_effects::lut_generation()
|
||||
}
|
||||
|
||||
/// How often the LUT files in use are stamped.
|
||||
pub const INTERVAL: Duration = Duration::from_secs(2);
|
||||
|
||||
/// A worker thread that stamps the LUT files in use ([`effectcraft_effects::check_lut_files`])
|
||||
/// and runs `wake` when one changed. Dropping the watcher ends the thread.
|
||||
pub struct Watcher {
|
||||
_stop: SyncSender<()>,
|
||||
}
|
||||
|
||||
impl Watcher {
|
||||
/// Start the worker. `None` where there are no threads (the web app, whose LUTs don't come
|
||||
/// from files) or one couldn't be started.
|
||||
pub fn start(wake: impl Fn() + Send + 'static) -> Option<Watcher> {
|
||||
Self::start_with(wake, INTERVAL)
|
||||
}
|
||||
|
||||
/// [`Watcher::start`] stamping every `every` (tests).
|
||||
pub fn start_with(wake: impl Fn() + Send + 'static, every: Duration) -> Option<Watcher> {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
return None;
|
||||
}
|
||||
let (tx, rx) = std::sync::mpsc::sync_channel::<()>(1);
|
||||
std::thread::Builder::new()
|
||||
.name("lut-watch".into())
|
||||
.spawn(move || {
|
||||
let mut seen = effectcraft_effects::lut_generation();
|
||||
// (ends once the watcher, the only sender, is dropped)
|
||||
while let Err(RecvTimeoutError::Timeout) = rx.recv_timeout(every) {
|
||||
let now = effectcraft_effects::check_lut_files();
|
||||
if now != seen {
|
||||
seen = now;
|
||||
wake();
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
Some(Watcher { _stop: tx })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A LUT file edited on disk wakes the interface without a render loading it.
|
||||
#[test]
|
||||
fn an_edited_lut_file_wakes_the_interface() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("watched.cube");
|
||||
std::fs::write(&path, "LUT_1D_SIZE 2\n0 0 0\n1 1 1\n").unwrap();
|
||||
let src = path.to_string_lossy().into_owned();
|
||||
assert!(effectcraft_effects::load_lut(&src).is_some());
|
||||
let before = effectcraft_effects::lut_generation();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let _watcher = Watcher::start_with(
|
||||
move || {
|
||||
let _ = tx.send(());
|
||||
},
|
||||
Duration::from_millis(10),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(&path, "LUT_1D_SIZE 3\n0 0 0\n0.5 0.5 0.5\n1 1 1\n").unwrap();
|
||||
rx.recv_timeout(Duration::from_secs(10)).unwrap();
|
||||
assert!(effectcraft_effects::lut_generation() > before);
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ pub use effectcraft_track as track;
|
||||
pub use footage::doc_path;
|
||||
pub use footage::footage_check;
|
||||
pub use footage::footage_reload;
|
||||
pub use footage::lut_reload;
|
||||
pub use footage::media_browser;
|
||||
pub use footage::media_cache;
|
||||
pub use footage::sequence;
|
||||
|
||||
@@ -97,6 +97,8 @@ impl EffectcraftApp {
|
||||
gpu_failure_applied: None,
|
||||
footage_watcher: None,
|
||||
footage_watch_key: None,
|
||||
lut_watcher: None,
|
||||
lut_generation: effectcraft_engine::lut_reload::generation(),
|
||||
device_lost_close: None,
|
||||
dialog_answers: std::sync::mpsc::channel(),
|
||||
file_dialogs: FileDialogsUp::default(),
|
||||
|
||||
@@ -654,6 +654,21 @@ impl EffectcraftApp {
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs(10));
|
||||
}
|
||||
self.tick_footage_reload(ctx);
|
||||
self.tick_lut_files(ctx);
|
||||
}
|
||||
|
||||
/// A LUT file in use changed on disk (#3493): the frames kept were rendered with the old one.
|
||||
/// Renders read it again by themselves; the watcher wakes the interface when no render did.
|
||||
fn tick_lut_files(&mut self, ctx: &egui::Context) {
|
||||
if self.lut_watcher.is_none() {
|
||||
let wake = ctx.clone();
|
||||
self.lut_watcher = effectcraft_engine::lut_reload::Watcher::start(move || wake.request_repaint());
|
||||
}
|
||||
let generation = effectcraft_engine::lut_reload::generation();
|
||||
if generation != self.lut_generation {
|
||||
self.lut_generation = generation;
|
||||
self.frames.clear_stale();
|
||||
}
|
||||
}
|
||||
|
||||
/// One background worker tracks source availability and optional content reloads. It wakes
|
||||
|
||||
@@ -298,6 +298,9 @@ struct Cache {
|
||||
playhead: Option<Playhead>,
|
||||
/// Scale of the frame on screen (the newest urgent request): frames of other scales go first.
|
||||
scale: Option<u32>,
|
||||
/// Frames whose render started before this are not kept: a source file they read changed
|
||||
/// since ([`Frames::clear_stale`]).
|
||||
stale_before: Option<web_time::Instant>,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
@@ -768,7 +771,7 @@ impl Frames {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let queue = Queue::default();
|
||||
Frames {
|
||||
cache: Arc::new(Mutex::new(Cache { map: HashMap::new(), order: VecDeque::new(), bytes: 0, gpu_bytes: 0, budget: 3 << 30, gpu_budget: GPU_BUDGET, gpu_pressure: None, gpu_adjusted: None, playhead: None, scale: None })),
|
||||
cache: Arc::new(Mutex::new(Cache { map: HashMap::new(), order: VecDeque::new(), bytes: 0, gpu_bytes: 0, budget: 3 << 30, gpu_budget: GPU_BUDGET, gpu_pressure: None, gpu_adjusted: None, playhead: None, scale: None, stale_before: None })),
|
||||
inflight: Arc::new(Mutex::new(HashSet::new())),
|
||||
queue: Arc::new(Mutex::new(queue)),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -1403,6 +1406,15 @@ impl Frames {
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop every frame, and the ones still rendering when they finish: a source file changed on
|
||||
/// disk (a LUT file, #3493), and a render that started before may have read the old one.
|
||||
pub fn clear_stale(&self) {
|
||||
self.clear();
|
||||
if let Ok(mut c) = self.cache.lock() {
|
||||
c.stale_before = Some(web_time::Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
/// The playing preview's position (`None` when no preview plays): prefetch renders the
|
||||
/// frames it reaches first before the others, evictions spare the frames it shows next, and
|
||||
/// queued prefetch frames of its series it no longer reaches are dropped (#595).
|
||||
@@ -1899,6 +1911,7 @@ impl Worker {
|
||||
}
|
||||
if let Ok(mut c) = self.cache.lock()
|
||||
&& (!img.is_gpu() || !self.gpu_retired.load(Ordering::Acquire))
|
||||
&& c.stale_before.is_none_or(|at| t0 >= at)
|
||||
{
|
||||
c.insert_with(key, img, files, since);
|
||||
}
|
||||
|
||||
@@ -218,6 +218,29 @@ fn retirement_reaches_existing_workers_and_keeps_cpu_frames_and_completion_live(
|
||||
assert!(frames.queue.lock().unwrap().running.is_empty());
|
||||
}
|
||||
|
||||
/// #3493: a LUT file changed on disk drops the frames kept, and a frame whose render started
|
||||
/// before is not kept when it finishes (it may have read the old file); one started after is.
|
||||
#[test]
|
||||
fn clear_stale_drops_kept_frames_and_ones_rendering() {
|
||||
let frames = Frames::default();
|
||||
let worker = frames.worker();
|
||||
let kept = key(1, 0, 0);
|
||||
insert(&frames, kept);
|
||||
let (late, fresh) = (key(1, 1, 0), key(1, 2, 0));
|
||||
let started = web_time::Instant::now();
|
||||
frames.clear_stale();
|
||||
assert!(!frames.is_cached(&kept));
|
||||
for k in [late, fresh] {
|
||||
frames.inflight.lock().unwrap().insert(k);
|
||||
frames.queue.lock().unwrap().running.insert(k);
|
||||
}
|
||||
worker.finish_key(late, false, img(), None, None, started);
|
||||
assert!(!frames.is_cached(&late), "a render started before the change");
|
||||
worker.finish_key(fresh, false, img(), None, None, web_time::Instant::now());
|
||||
assert!(frames.is_cached(&fresh), "a render started after it");
|
||||
assert_eq!(frames.inflight(), 0, "both jobs are released");
|
||||
}
|
||||
|
||||
fn key(content: u64, frame: i64, opts: u64) -> FrameKey {
|
||||
FrameKey { revision: content, content, comp: 1, frame, scale: 1000, view: 0, opts }
|
||||
}
|
||||
|
||||
@@ -373,6 +373,10 @@ pub struct EffectcraftApp {
|
||||
footage_watcher: Option<effectcraft_engine::footage_reload::Watcher>,
|
||||
/// The project revision and preference the watched set was last computed for.
|
||||
footage_watch_key: Option<(u64, String, bool)>,
|
||||
/// The worker that stamps the LUT files in use ([`Self::tick_lut_files`], #3493).
|
||||
lut_watcher: Option<effectcraft_engine::lut_reload::Watcher>,
|
||||
/// The LUT generation the frames kept were rendered at ([`effectcraft_engine::lut_reload::generation`]).
|
||||
lut_generation: u64,
|
||||
/// The graphics device was lost and the window is closing (see `close_after_device_loss`);
|
||||
/// `true` keeps crash recovery for the next launch.
|
||||
device_lost_close: Option<bool>,
|
||||
|
||||
Reference in new issue
Block a user