From 506a5b327f07c43cf1e61307022c37a9c6711059 Mon Sep 17 00:00:00 2001 From: arnav-hiwarkar Date: Wed, 21 Oct 2026 21:47:18 +0530 Subject: [PATCH] Timeline: the disk-cache bar checks only the visible frames - `Frames::disk_frames` takes a frame range instead of a frame count, still clamped to the existing 100,000-frame limit. - The Timeline passes the visible range (`disk_bar_range`), including frames only partly in view, and clips the blue bar to the work-area row. - Membership stays live: nothing is cached, so the bar updates as before when frames are written, evicted or promoted to RAM. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/frames.rs | 109 +++++++++++++++++++-- crates/ui-egui/src/panels/timeline.rs | 38 ++++++- crates/ui-egui/tests/ui_web_gpu_storage.rs | 2 +- 3 files changed, 139 insertions(+), 10 deletions(-) diff --git a/crates/ui-egui/src/frames.rs b/crates/ui-egui/src/frames.rs index 059bc3a0..7e4036a6 100644 --- a/crates/ui-egui/src/frames.rs +++ b/crates/ui-egui/src/frames.rs @@ -758,7 +758,12 @@ impl Frames { /// Frames of the series of `base` in the disk cache but not in RAM — the blue cache bar. /// `opts` are the viewer's render options (part of the disk key). Native queries return /// no blue frames until the content key is ready; hashing never blocks timeline painting. - pub fn disk_frames(&self, src: &RenderSource, base: &FrameKey, frames: i64, opts: &RenderOpts) -> Vec { + pub fn disk_frames(&self, src: &RenderSource, base: &FrameKey, range: std::ops::Range, opts: &RenderOpts) -> Vec { + let start = range.start.clamp(0, 100_000); + let end = range.end.clamp(0, 100_000); + if start >= end { + return vec![]; + } let remote = self.remote.as_ref().filter(|r| r.disk()); let contains = |k: u128| match (&src.disk, remote) { (Some(dc), _) => dc.contains(disk_cache::Kind::Frame, k), @@ -781,10 +786,7 @@ impl Frames { let content = content_key(&self.content_keys, &src.project, base.revision, base.comp); let ram: HashSet = self.cached_frames(base).into_iter().collect(); let oh = opts_hash(opts); - (0..frames.clamp(0, 100_000)) - .filter(|f| !ram.contains(f)) - .filter(|f| contains(disk_cache::frame_key(content, *f, base.scale, base.view ^ oh))) - .collect() + (start..end).filter(|f| !ram.contains(f)).filter(|f| contains(disk_cache::frame_key(content, *f, base.scale, base.view ^ oh))).collect() } #[cfg(not(target_arch = "wasm32"))] @@ -1375,6 +1377,54 @@ impl Queue { mod tests { use super::*; + struct ProbeDisk { + keys: Mutex>, + probes: std::sync::atomic::AtomicUsize, + } + + impl RemoteFrames for ProbeDisk { + fn slots(&self) -> usize { + 0 + } + + fn disk(&self) -> bool { + true + } + + fn disk_contains(&self, key: u128) -> bool { + self.probes.fetch_add(1, Ordering::Relaxed); + self.keys.lock().unwrap().contains(&key) + } + + fn start(&self, _: RemoteJob, _: RemoteDone) {} + } + + fn disk_fixture(stored_frames: &[i64]) -> (Frames, RenderSource, FrameKey, RenderOpts, Arc, u128) { + let mut session = effectcraft_engine::Session::default(); + session.execute("comp.new", serde_json::json!({"width": 32, "height": 18, "duration": 1})).unwrap(); + let comp = session.active_comp_id().unwrap(); + let opts = RenderOpts::default(); + let content = 1234; + let mut base = key(1, 0, opts_hash(&opts)); + base.revision = 7; + base.comp = comp.0; + let disk = Arc::new(ProbeDisk { keys: Mutex::new(HashSet::new()), probes: 0.into() }); + disk.keys.lock().unwrap().extend(stored_frames.iter().map(|frame| disk_cache::frame_key(content, *frame, base.scale, base.view ^ opts_hash(&opts)))); + let mut frames = Frames::default(); + frames.set_remote(Some(disk.clone())); + frames.content_keys.lock().unwrap().insert((base.revision, base.comp), content); + let source = RenderSource { + project: session.project.clone(), + footage: session.footage.clone(), + expr: None, + layer_cache: Arc::default(), + gpu: None, + gpu_display: false, + disk: None, + }; + (frames, source, base, opts, disk, content) + } + #[cfg(not(target_arch = "wasm32"))] #[test] fn disk_indicator_does_not_hash_on_ui_thread_and_coalesces_edits() { @@ -1422,7 +1472,7 @@ mod tests { base.comp = cid.0; for revision in 1..50 { base.revision = revision; - assert!(frames.disk_frames(&src, &base, 30, &opts).is_empty()); + assert!(frames.disk_frames(&src, &base, 0..30, &opts).is_empty()); } assert!(frames.content_keys.lock().unwrap().is_empty(), "painting computed a content hash while the worker was blocked"); assert_eq!(frames.content_key_jobs.lock().unwrap().pending.len(), 1); @@ -1432,10 +1482,55 @@ mod tests { assert!(Instant::now() < until); std::thread::sleep(Duration::from_millis(1)); } - assert_eq!(frames.disk_frames(&src, &base, 30, &opts), vec![2], "the blue indicator must return once its key is ready"); + assert_eq!(frames.disk_frames(&src, &base, 0..30, &opts), vec![2], "the blue indicator must return once its key is ready"); assert_eq!(frames.content_keys.lock().unwrap().len(), 1, "superseded revisions were hashed"); } + #[test] + fn disk_indicator_range_tracks_live_membership() { + let (frames, src, base, opts, disk, _) = disk_fixture(&[10, 12]); + assert_eq!(frames.disk_frames(&src, &base, 10..13, &opts), vec![10, 12]); + assert_eq!(disk.probes.load(Ordering::Relaxed), 3); + + disk.keys.lock().unwrap().insert(disk_cache::frame_key(1234, 11, base.scale, base.view ^ opts_hash(&opts))); + assert_eq!(frames.disk_frames(&src, &base, 10..13, &opts), vec![10, 11, 12]); + + insert(&frames, FrameKey { frame: 11, ..base }); + disk.probes.store(0, Ordering::Relaxed); + assert_eq!(frames.disk_frames(&src, &base, 10..13, &opts), vec![10, 12]); + assert_eq!(disk.probes.load(Ordering::Relaxed), 2); + + disk.keys.lock().unwrap().remove(&disk_cache::frame_key(1234, 12, base.scale, base.view ^ opts_hash(&opts))); + assert_eq!(frames.disk_frames(&src, &base, 10..13, &opts), vec![10]); + + let mut revised = base; + revised.revision += 1; + frames.content_keys.lock().unwrap().insert((revised.revision, revised.comp), 5678); + assert!(frames.disk_frames(&src, &revised, 10..13, &opts).is_empty()); + + let changed = FrameKey { scale: base.scale + 1, ..base }; + assert!(frames.disk_frames(&src, &changed, 10..13, &opts).is_empty()); + let changed = FrameKey { view: base.view + 1, ..base }; + assert!(frames.disk_frames(&src, &changed, 10..13, &opts).is_empty()); + assert!(frames.disk_frames(&src, &base, 10..13, &RenderOpts { draft: true, ..opts }).is_empty()); + } + + #[test] + fn disk_indicator_range_is_bounded() { + let (frames, src, base, opts, disk, _) = disk_fixture(&[0, 1, 99_999]); + assert_eq!(frames.disk_frames(&src, &base, -5..2, &opts), vec![0, 1]); + assert_eq!(disk.probes.load(Ordering::Relaxed), 2); + disk.probes.store(0, Ordering::Relaxed); + assert_eq!(frames.disk_frames(&src, &base, 99_999..i64::MAX, &opts), vec![99_999]); + assert_eq!(disk.probes.load(Ordering::Relaxed), 1); + disk.probes.store(0, Ordering::Relaxed); + assert!(frames.disk_frames(&src, &base, 2..2, &opts).is_empty()); + // Written as a struct literal: `3..2` trips clippy::reversed_empty_ranges. + let reversed = std::ops::Range { start: 3, end: 2 }; + assert!(frames.disk_frames(&src, &base, reversed, &opts).is_empty()); + assert_eq!(disk.probes.load(Ordering::Relaxed), 0); + } + /// Frames larger than the texture limit are averaged down by a whole factor (#201: an /// 11000×2200 comp at Full resolution against an 8192 limit). #[test] diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index b34fbd9d..6ba329e5 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -515,6 +515,19 @@ impl TMap { } } +fn disk_bar_range(tm: &TMap, clip: Rect, fd: f64, frames: i64) -> std::ops::Range { + if clip.width() <= 0.0 || clip.height() <= 0.0 || !fd.is_finite() || fd <= 0.0 || !tm.pps.is_finite() || tm.pps <= 0.0 { + return 0..0; + } + let start = tm.t(clip.min.x) / fd; + let end = tm.t(clip.max.x) / fd; + if !start.is_finite() || !end.is_finite() { + return 0..0; + } + let limit = frames.clamp(0, 100_000) as f64; + (start.clamp(0.0, limit).floor() as i64)..(end.clamp(0.0, limit).ceil() as i64) +} + /// A Timeline ruler tick label for comp frame `fno`: seconds:frames (`00:15f`, AE) or, when /// the project's Time Display Style is Frames, the frame number the current-time display shows /// (`00015`, counted from Start Numbering Frames At) (#710). @@ -1674,11 +1687,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { if app.session.disk_cache.is_some() || app.frames.remote_disk() { let frames = comp.frame_rate.frame_at(comp.duration); let opts = app.frame_opts(cid, series.scale as f64 / 1000.0); - let disk = app.frames.disk_frames(&app.render_source(), &series, frames, &opts); + let clip = wa_row.intersect(p.clip_rect()); + let range = disk_bar_range(&tm, clip, fd, frames); + let disk = app.frames.disk_frames(&app.render_source(), &series, range, &opts); + let blue_painter = p.with_clip_rect(clip); let draw_blue = |a: i64, b: i64| { let x0 = tm.x(a as f64 * fd); let x1 = tm.x((b + 1) as f64 * fd); - p.rect_filled(Rect::from_min_max(pos2(x0, cy0), pos2(x1, cy0 + 2.5)), 0.0, t.cache_blue); + blue_painter.rect_filled(Rect::from_min_max(pos2(x0, cy0), pos2(x1, cy0 + 2.5)), 0.0, t.cache_blue); }; let mut run: Option<(i64, i64)> = None; for f in disk { @@ -4720,4 +4736,22 @@ mod tests { let sel = bar_color(red, true); assert!(sel.r() > bar.r()); } + + #[test] + fn disk_bar_range_covers_partial_frames() { + let clip = Rect::from_min_max(pos2(0.0, 0.0), pos2(2.0, 1.0)); + let tm = TMap { x0: 0.0, start: 10.25, pps: 1.0 }; + assert_eq!(disk_bar_range(&tm, clip, 1.0, 100), 10..13); + + let exact = TMap { start: 10.0, ..tm }; + assert_eq!(disk_bar_range(&exact, clip, 1.0, 100), 10..12); + let off_comp = TMap { start: 200.0, ..tm }; + assert_eq!(disk_bar_range(&off_comp, clip, 1.0, 100), 100..100); + assert_eq!(disk_bar_range(&tm, clip, f64::NAN, 100), 0..0); + assert_eq!(disk_bar_range(&TMap { pps: 0.0, ..tm }, clip, 1.0, 100), 0..0); + assert_eq!(disk_bar_range(&TMap { start: f64::NAN, ..tm }, clip, 1.0, 100), 0..0); + assert_eq!(disk_bar_range(&tm, Rect::from_min_max(pos2(2.0, 0.0), pos2(2.0, 1.0)), 1.0, 100), 0..0); + let above_limit = TMap { start: 100_001.0, ..tm }; + assert_eq!(disk_bar_range(&above_limit, clip, 1.0, i64::MAX), 100_000..100_000); + } } diff --git a/crates/ui-egui/tests/ui_web_gpu_storage.rs b/crates/ui-egui/tests/ui_web_gpu_storage.rs index d706a382..9977858c 100644 --- a/crates/ui-egui/tests/ui_web_gpu_storage.rs +++ b/crates/ui-egui/tests/ui_web_gpu_storage.rs @@ -172,7 +172,7 @@ fn gpu_worker_renders_effect_frames_and_the_disk_cache_survives_a_reload() { assert!(frames2.remote_disk()); let until = std::time::Instant::now() + std::time::Duration::from_secs(5); let bar = loop { - let bar = frames2.disk_frames(&source(&s, None), &key(fx), 30, &opts); + let bar = frames2.disk_frames(&source(&s, None), &key(fx), 0..30, &opts); if !bar.is_empty() { break bar; }