Merge #2048: Timeline: the disk-cache bar checks only the visible frames (@arnav-hiwarkar)

This commit is contained in:
aiongg
2026-10-23 21:37:28 +00:00
3 changed files with 139 additions and 10 deletions
+102 -7
View File
@@ -784,7 +784,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<i64> {
pub fn disk_frames(&self, src: &RenderSource, base: &FrameKey, range: std::ops::Range<i64>, opts: &RenderOpts) -> Vec<i64> {
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),
@@ -808,10 +813,7 @@ impl Frames {
let content = content_key(&self.content_keys, &src.project, base.revision, base.comp, epoch, self.virtual_id.as_deref());
let ram: HashSet<i64> = 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"))]
@@ -1437,6 +1439,54 @@ impl Queue {
mod tests {
use super::*;
struct ProbeDisk {
keys: Mutex<HashSet<u128>>,
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<ProbeDisk>, 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, 0), 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() {
@@ -1484,7 +1534,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);
@@ -1494,10 +1544,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, 0), 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]
+36 -2
View File
@@ -517,6 +517,19 @@ impl TMap {
}
}
fn disk_bar_range(tm: &TMap, clip: Rect, fd: f64, frames: i64) -> std::ops::Range<i64> {
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).
@@ -1738,11 +1751,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 {
@@ -4891,4 +4907,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);
}
}
@@ -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;
}