multicam: throttle angle refresh, shrink pre-render window

- MulticamPanel: playback angle refresh runs one cycle per 3 ticks
  instead of re-requesting every source every tick — each angle decode
  is a keyframe-scanning FFmpeg seek that stole worker capacity from
  the main viewer
- PreRender frames default 120 -> 12: a window larger than what the
  pool can render in real time queues far ahead of the playhead, so
  the painted frame lags seconds behind (playback frozen); a smaller
  window keeps the backlog bounded
- render_graph_frame: OAK_PERF clip-level timing
This commit is contained in:
2026-09-01 21:52:56 +08:00
parent 7f41570596
commit be42620e22
3 changed files with 49 additions and 8 deletions
+8 -2
View File
@@ -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";
+27 -5
View File
@@ -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<E: AppEngine> {
play_queue: VecDeque<QueuedSwitch>,
/// 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<E: AppEngine> MulticamPanel<E> {
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<E: AppEngine> MulticamPanel<E> {
}
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;
+14 -1
View File
@@ -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<Frame> = 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