diff --git a/crates/oak-app/src/oakui/real.rs b/crates/oak-app/src/oakui/real.rs index 4a2b477c2..eb06e6efa 100644 --- a/crates/oak-app/src/oakui/real.rs +++ b/crates/oak-app/src/oakui/real.rs @@ -246,8 +246,14 @@ enum FullResTarget { // that fall out of the window (or whose params were invalidated) release // their slots back to the workers. -/// The default forward pre-render window (frames). -pub const DEFAULT_PREVIEW_WINDOW_FORWARD: i64 = 120; +/// The default forward pre-render window (frames). Kept small: each +/// frame is slow on some hardware (multi-clip montage decodes through +/// FFmpeg), and a large window queues far ahead of the playhead — the +/// workers never catch up, the painted frame lags seconds behind the +/// crown and playback looks frozen. A shorter window bounds the backlog +/// to what the pool can render in a fraction of a second, so the +/// displayed frame tracks the playhead instead of chasing it. +pub const DEFAULT_PREVIEW_WINDOW_FORWARD: i64 = 12; /// Config key for the forward pre-render window size (frames). pub const CONFIG_KEY_PREVIEW_WINDOW: &str = "PlaybackPreRenderFrames"; diff --git a/crates/oak-app/src/panels/multicam.rs b/crates/oak-app/src/panels/multicam.rs index 7cb10782f..dba516255 100644 --- a/crates/oak-app/src/panels/multicam.rs +++ b/crates/oak-app/src/panels/multicam.rs @@ -61,6 +61,13 @@ use crate::panels::chip; /// The number of sources refreshed per tick during playback (the rest keep /// their last frame until their turn — the task's round-robin throttle). const PLAYBACK_REFRESH_PER_TICK: i32 = 2; +/// A refresh cycle runs once every this many ticks (a tick is one frame +/// at the sequence rate). Decoding one angle is an FFmpeg keyframe- +/// scanning seek (~10-50 ms even on a 720p proxy); re-requesting ALL +/// sources EVERY tick made those seek costs compound and starved the +/// main viewer's worker pool. The grid still refreshes each source in +/// turn, just at a third of the frame rate. +const PLAYBACK_REFRESH_STRIDE: i32 = 3; /// One pending switch (the C++ `MulticamWidget`'s `play_queue_`): the /// switch applies once the program playhead reaches `target`. @@ -92,6 +99,10 @@ pub struct MulticamPanel { play_queue: VecDeque, /// Round-robin cursor over the sources (playback refresh throttle). refresh_cursor: i32, + /// Tick counter for the playback refresh cycle (stride-scaled). + refresh_tick_toggle: u64, + /// The refresh cycle seen last (dedups re-requests within a cycle). + last_refresh_cycle: i64, /// The last program playhead (detects a jump / rest). last_playhead: i64, /// Set when a switch cleared the frames: the next refresh pass covers @@ -121,6 +132,8 @@ impl MulticamPanel { frames: HashMap::new(), play_queue: VecDeque::new(), refresh_cursor: 0, + refresh_tick_toggle: 0, + last_refresh_cycle: -1, last_playhead: i64::MIN, full_refresh: true, focus: _cx.focus_handle(), @@ -177,12 +190,21 @@ impl MulticamPanel { } self.refresh_cursor = 0; } else { - // Playback: round-robin a couple of sources per tick. - for _ in 0..state.source_count.min(PLAYBACK_REFRESH_PER_TICK) { - let source = self.refresh_cursor % state.source_count; - self.refresh_cursor += 1; - self.request_angle(source, cx); + // Playback: one refresh cycle per PLAYBACK_REFRESH_STRIDE + // ticks, round-robin a couple of sources per cycle (the old + // per-tick round-robin requested decodes faster than they can + // complete — each is a keyframe-scanning seek and stole the + // main viewer's worker capacity). + let cycle = (self.refresh_tick_toggle / PLAYBACK_REFRESH_STRIDE as u64) as i64; + if cycle != self.last_refresh_cycle { + self.last_refresh_cycle = cycle; + for _ in 0..state.source_count.min(PLAYBACK_REFRESH_PER_TICK) { + let source = self.refresh_cursor % state.source_count; + self.refresh_cursor += 1; + self.request_angle(source, cx); + } } + self.refresh_tick_toggle = self.refresh_tick_toggle.wrapping_add(1); } self.full_refresh = false; self.last_playhead = playhead; diff --git a/crates/oak-render/src/eval.rs b/crates/oak-render/src/eval.rs index 431aeb091..7f5110497 100644 --- a/crates/oak-render/src/eval.rs +++ b/crates/oak-render/src/eval.rs @@ -1304,11 +1304,21 @@ pub fn render_graph_frame( let mut perf_collect = perf.then(std::time::Instant::now); let mut perf_collect_ms = 0.0f64; let mut frames: Vec = Vec::new(); - for clip in clips { + let mut perf_clip_hist: Vec<(oak_node::id::NodeId, f64)> = Vec::new(); + for clip in clips.clone() { + let clip_sw = perf.then(std::time::Instant::now); let request = oak_node::traverser::EvalRequest::new(clip, time); let table = traverser.evaluate(graph, &request, &mut hooks).map_err(|e| { Error::Failed(format!("graph evaluation of clip {clip:?} failed: {e:?}")) })?; + if perf { + perf_clip_hist.push(( + clip, + clip_sw + .map(|t| t.elapsed().as_secs_f64() * 1000.0) + .unwrap_or(0.0), + )); + } if let Some(t0) = &perf_collect { perf_collect_ms += t0.elapsed().as_secs_f64() * 1000.0; perf_collect = Some(std::time::Instant::now()); @@ -1337,6 +1347,9 @@ pub fn render_graph_frame( let composite_ms = composite_started .map(|t| t.elapsed().as_secs_f64() * 1000.0) .unwrap_or(0.0); + for (clip, ms) in &perf_clip_hist { + eprintln!("[perf] clip {clip:?} evaluate {ms:.1}ms"); + } eprintln!( "[perf] graph frame time {time:?} size {size:?}: evaluate {perf_collect_ms:.1}ms composite {composite_ms:.1}ms total {}ms", perf_collect_ms + composite_ms