mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Merge pull request #1121 from pravdomil/claude/ram-preview-prefetch-bug-873f91
#595: RAM preview no longer stalls on a gap right after the playhead
This commit is contained in:
@@ -406,7 +406,8 @@ impl EffectcraftApp {
|
||||
}
|
||||
let cur = comp.frame_rate.frame_at(self.session.time()).clamp(wa, wb);
|
||||
let fit = self.frames_that_fit(comp, series.scale as f64 / 1000.0).min(n);
|
||||
let slots = (self.frames_parallelism() / 2).max(1);
|
||||
// One thread each (`Frames::request_background`), so every frame thread takes one.
|
||||
let slots = self.frames_parallelism().max(1);
|
||||
let mut queued = self.frames.inflight();
|
||||
let scale = series.scale as f64 / 1000.0;
|
||||
let mut pending = false;
|
||||
@@ -419,7 +420,7 @@ impl EffectcraftApp {
|
||||
if queued >= slots {
|
||||
break;
|
||||
}
|
||||
self.request_frame(ItemId(series.comp), f, scale);
|
||||
self.request_frame_background(ItemId(series.comp), f, scale);
|
||||
queued += 1;
|
||||
}
|
||||
if pending {
|
||||
|
||||
@@ -126,12 +126,18 @@ impl EffectcraftApp {
|
||||
|
||||
/// Request a prefetch frame render (no-op if cached/in flight).
|
||||
pub fn request_frame(&self, comp: ItemId, frame: i64, scale: f64) {
|
||||
self.request_frame_with(comp, frame, scale, false);
|
||||
self.request_frame_with(comp, frame, scale, frames::Frames::request);
|
||||
}
|
||||
|
||||
/// Request the frame on screen: rendered before any queued prefetch.
|
||||
pub fn request_frame_urgent(&self, comp: ItemId, frame: i64, scale: f64) {
|
||||
self.request_frame_with(comp, frame, scale, true);
|
||||
self.request_frame_with(comp, frame, scale, frames::Frames::request_urgent);
|
||||
}
|
||||
|
||||
/// Request a frame nobody waits for (Cache Frames When Idle): rendered on one thread
|
||||
/// (`Frames::request_background`).
|
||||
pub fn request_frame_background(&self, comp: ItemId, frame: i64, scale: f64) {
|
||||
self.request_frame_with(comp, frame, scale, frames::Frames::request_background);
|
||||
}
|
||||
|
||||
/// Render options of viewer frames of `comp` at `scale`.
|
||||
@@ -152,15 +158,17 @@ impl EffectcraftApp {
|
||||
}
|
||||
}
|
||||
|
||||
fn request_frame_with(&self, comp: ItemId, frame: i64, scale: f64, urgent: bool) {
|
||||
fn request_frame_with(
|
||||
&self,
|
||||
comp: ItemId,
|
||||
frame: i64,
|
||||
scale: f64,
|
||||
request: fn(&frames::Frames, &frames::RenderSource, FrameKey, ItemId, Tick, RenderOpts),
|
||||
) {
|
||||
let Some(c) = self.session.project.comp(comp) else { return };
|
||||
let key = self.frame_key(comp, frame, scale);
|
||||
let t = c.frame_rate.tick_of(frame);
|
||||
let opts = self.frame_opts(comp, scale);
|
||||
if urgent {
|
||||
self.frames.request_urgent(&self.render_source(), key, comp, t, opts);
|
||||
} else {
|
||||
self.frames.request(&self.render_source(), key, comp, t, opts);
|
||||
}
|
||||
request(&self.frames, &self.render_source(), key, comp, t, opts);
|
||||
}
|
||||
}
|
||||
|
||||
+156
-28
@@ -1,7 +1,8 @@
|
||||
//! Background frame rendering and the RAM preview cache.
|
||||
//!
|
||||
//! Frames render on a dedicated thread pool from immutable `Arc<Project>` snapshots, so the UI
|
||||
//! never waits for the compositor. Results land in a memory-budgeted cache keyed by the comp's
|
||||
//! never waits for the compositor (a playing preview's frames and idle caching on one thread
|
||||
//! each, so the frames started first are done first). Results land in a memory-budgeted cache keyed by the comp's
|
||||
//! *content* ([`comp_content`]: the comp and everything it uses), frame, scale, view and render
|
||||
//! options; the timeline draws the cached range as the green cache bar. An edit keeps the frames
|
||||
//! of every comp it doesn't touch, and undo finds the frames of the state it returns to.
|
||||
@@ -456,6 +457,10 @@ pub struct Frames {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// None if the OS refused the threads: jobs then go to rayon's global pool.
|
||||
pool: Option<rayon::ThreadPool>,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// One thread each, for a playing preview's frames and idle caching (see [`Queue::idle_lanes`]). Empty if
|
||||
/// the OS refused the threads: those frames then render on `pool` too.
|
||||
lanes: Vec<rayon::ThreadPool>,
|
||||
ctx: Option<egui::Context>,
|
||||
/// Render time of the last viewer (urgent) frame in ms, for the Info panel / perf readout.
|
||||
pub last_ms: Arc<Mutex<f64>>,
|
||||
@@ -484,10 +489,25 @@ impl Default for Frames {
|
||||
fn default() -> Self {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let threads = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4).clamp(2, 16);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let lanes: Vec<rayon::ThreadPool> = (0..threads)
|
||||
.map_while(|i| {
|
||||
rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(1)
|
||||
.thread_name(move |_| format!("ec-preview-{i}"))
|
||||
.panic_handler(|_| log::error!("a frame render panicked; that frame was skipped"))
|
||||
.build()
|
||||
.ok()
|
||||
})
|
||||
.collect();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let queue = Queue { idle_lanes: (0..lanes.len()).collect(), lanes: lanes.len(), ..Default::default() };
|
||||
#[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, playhead: None, scale: None })),
|
||||
inflight: Arc::new(Mutex::new(HashSet::new())),
|
||||
queue: Arc::new(Mutex::new(Queue::default())),
|
||||
queue: Arc::new(Mutex::new(queue)),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
// A panicking job would abort the whole process (rayon's default for `spawn`): log it
|
||||
// and lose only that frame (the panic hook has logged the message).
|
||||
@@ -497,6 +517,8 @@ impl Default for Frames {
|
||||
.panic_handler(|_| log::error!("a frame render panicked; that frame was skipped"))
|
||||
.build()
|
||||
.ok(),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
lanes,
|
||||
ctx: None,
|
||||
last_ms: Arc::new(Mutex::new(0.0)),
|
||||
content_keys: Arc::default(),
|
||||
@@ -911,26 +933,77 @@ impl Frames {
|
||||
c.playhead = playhead;
|
||||
}
|
||||
let Ok(mut q) = self.queue.lock() else { return };
|
||||
// A preview starting or stopping moves the queued prefetch frames between the lanes and
|
||||
// the pool (see [`Queue::idle_lanes`]).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let moved = q.playhead.is_some() != playhead.is_some();
|
||||
q.playhead = playhead;
|
||||
let Some(p) = playhead else { return };
|
||||
let Ok(mut inf) = self.inflight.lock() else { return };
|
||||
q.jobs.retain(|j| {
|
||||
let gone = !j.urgent && j.key.same_series(&p.series) && p.ahead(&j.key).is_none();
|
||||
if gone {
|
||||
inf.remove(&j.key);
|
||||
if let Some(p) = playhead
|
||||
&& let Ok(mut inf) = self.inflight.lock()
|
||||
{
|
||||
q.jobs.retain(|j| {
|
||||
let gone = !j.urgent && j.key.same_series(&p.series) && p.ahead(&j.key).is_none();
|
||||
if gone {
|
||||
inf.remove(&j.key);
|
||||
}
|
||||
!gone
|
||||
});
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if moved {
|
||||
self.start_lanes(&mut q, usize::MAX);
|
||||
let pool_jobs = q.jobs.iter().filter(|j| !q.on_lane(j)).count().min(self.parallelism());
|
||||
drop(q);
|
||||
for _ in 0..pool_jobs {
|
||||
self.start_pool();
|
||||
}
|
||||
!gone
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Start up to `n` idle lanes on the queued jobs (see [`Queue::idle_lanes`]).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn start_lanes(&self, q: &mut Queue, n: usize) {
|
||||
if self.remote.is_some() {
|
||||
return;
|
||||
}
|
||||
let jobs = q.jobs.iter().filter(|j| q.on_lane(j)).count();
|
||||
for _ in 0..n.min(jobs) {
|
||||
let Some(lane) = q.idle_lanes.pop() else { break };
|
||||
let Some(pool) = self.lanes.get(lane) else { break };
|
||||
let w = self.worker();
|
||||
pool.spawn(move || w.drain(Some(lane)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Have a pool thread render the queued jobs that are the pool's.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn start_pool(&self) {
|
||||
if self.remote.is_some() {
|
||||
return;
|
||||
}
|
||||
let w = self.worker();
|
||||
let job = move || w.drain(None);
|
||||
match &self.pool {
|
||||
Some(pool) => pool.spawn(job),
|
||||
None => rayon::spawn(job),
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue a prefetch frame (no-op if cached or already queued/rendering).
|
||||
pub fn request(&self, src: &RenderSource, key: FrameKey, comp: ItemId, t: Tick, opts: RenderOpts) {
|
||||
self.request_with(src, key, comp, t, opts, false);
|
||||
self.request_with(src, key, comp, t, opts, Want::Prefetch);
|
||||
}
|
||||
|
||||
/// Queue a frame nobody waits for (Cache Frames When Idle): as [`Frames::request`], but it
|
||||
/// renders on a lane, one thread, and leaves the pool to the viewer's frame (see
|
||||
/// [`Queue::idle_lanes`]).
|
||||
pub fn request_background(&self, src: &RenderSource, key: FrameKey, comp: ItemId, t: Tick, opts: RenderOpts) {
|
||||
self.request_with(src, key, comp, t, opts, Want::Background);
|
||||
}
|
||||
|
||||
/// Queue the frame the viewer is showing: it jumps ahead of every prefetch job.
|
||||
pub fn request_urgent(&self, src: &RenderSource, key: FrameKey, comp: ItemId, t: Tick, opts: RenderOpts) {
|
||||
self.request_with(src, key, comp, t, opts, true);
|
||||
self.request_with(src, key, comp, t, opts, Want::Viewer);
|
||||
}
|
||||
|
||||
/// An urgent (viewer) frame is queued or rendering.
|
||||
@@ -938,7 +1011,8 @@ impl Frames {
|
||||
self.queue.lock().map(|q| q.urgent_running > 0 || q.jobs.iter().any(|j| j.urgent)).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn request_with(&self, src: &RenderSource, key: FrameKey, comp: ItemId, t: Tick, opts: RenderOpts, urgent: bool) {
|
||||
fn request_with(&self, src: &RenderSource, key: FrameKey, comp: ItemId, t: Tick, opts: RenderOpts, want: Want) {
|
||||
let urgent = want == Want::Viewer;
|
||||
// The frame on screen sets the scale evictions keep: a prefetch or a passive viewer at
|
||||
// another zoom must not evict the active viewer's frames.
|
||||
let cached = self
|
||||
@@ -990,22 +1064,32 @@ impl Frames {
|
||||
}
|
||||
}
|
||||
if !inf.insert(key) {
|
||||
drop(inf);
|
||||
drop(q);
|
||||
// Queued for a lane and now the viewer's frame: it is the pool's.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if urgent {
|
||||
self.start_pool();
|
||||
}
|
||||
return;
|
||||
}
|
||||
q.seq += 1;
|
||||
let seq = q.seq;
|
||||
q.jobs.push(Job { key, src: src.clone(), comp, t, opts, urgent, seq, cancel: Arc::default() });
|
||||
let job = Job { key, src: src.clone(), comp, t, opts, urgent, background: want == Want::Background, seq, cancel: Arc::default() };
|
||||
// A lane's frame: a busy lane takes the next job when it is done, an idle one starts now.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let on_lane = q.on_lane(&job);
|
||||
q.jobs.push(job);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if on_lane {
|
||||
self.start_lanes(&mut q, 1);
|
||||
}
|
||||
drop(inf);
|
||||
drop(q);
|
||||
// One spawn per job; each spawn renders whichever queued jobs matter most right now.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.remote.is_none() {
|
||||
let w = self.worker();
|
||||
// One spawn per job; each spawn renders whichever queued jobs matter most right now.
|
||||
let job = move || w.drain();
|
||||
match &self.pool {
|
||||
Some(pool) => pool.spawn(job),
|
||||
None => rayon::spawn(job),
|
||||
}
|
||||
if !on_lane {
|
||||
self.start_pool();
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
if let Some(ctx) = &self.ctx {
|
||||
@@ -1147,7 +1231,7 @@ impl Frames {
|
||||
}
|
||||
let t0 = web_time::Instant::now();
|
||||
let w = self.worker();
|
||||
while w.run_next() {
|
||||
while w.run_next(None) {
|
||||
if t0.elapsed() >= budget {
|
||||
break;
|
||||
}
|
||||
@@ -1195,22 +1279,29 @@ impl Worker {
|
||||
/// every UI frame, so renders piled up on every thread until memory ran out (#647). A spawn
|
||||
/// that lands inside a render returns at once instead; the render below it takes the queued
|
||||
/// jobs when it is done, so each thread renders one frame at a time.
|
||||
///
|
||||
/// `lane`: the lane this runs on (see [`Queue::idle_lanes`]), `None` on the pool.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn drain(&self) {
|
||||
fn drain(&self, lane: Option<usize>) {
|
||||
if DRAINING.get() {
|
||||
return;
|
||||
}
|
||||
DRAINING.set(true);
|
||||
let _reset = Draining;
|
||||
while self.run_next() {}
|
||||
while self.run_next(lane) {}
|
||||
}
|
||||
|
||||
/// Render the queued job that matters most right now. `false` when the queue was empty.
|
||||
fn run_next(&self) -> bool {
|
||||
/// Render the queued job that matters most right now, on lane `lane` (`None`: on
|
||||
/// the pool, or the UI thread). `false` when the queue had no job for it; the lane is then
|
||||
/// idle again.
|
||||
fn run_next(&self, lane: Option<usize>) -> bool {
|
||||
let Worker { queue, content_keys, .. } = self;
|
||||
let job = {
|
||||
let Ok(mut q) = queue.lock() else { return false };
|
||||
let Some(i) = q.best() else { return false };
|
||||
let Some(i) = q.next(lane.is_some()) else {
|
||||
q.idle_lanes.extend(lane);
|
||||
return false;
|
||||
};
|
||||
let job = q.jobs.swap_remove(i);
|
||||
q.running.insert(job.key);
|
||||
if job.urgent {
|
||||
@@ -1405,6 +1496,14 @@ impl Worker {
|
||||
}
|
||||
}
|
||||
|
||||
/// Who asked for a frame (see [`Frames::request_with`]).
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Want {
|
||||
Viewer,
|
||||
Prefetch,
|
||||
Background,
|
||||
}
|
||||
|
||||
struct Job {
|
||||
key: FrameKey,
|
||||
src: RenderSource,
|
||||
@@ -1412,6 +1511,8 @@ struct Job {
|
||||
t: Tick,
|
||||
opts: RenderOpts,
|
||||
urgent: bool,
|
||||
/// Nobody waits for it (see [`Frames::request_background`]).
|
||||
background: bool,
|
||||
seq: u64,
|
||||
/// Set when the viewer no longer waits for this frame (see [`Frames::request_urgent`]).
|
||||
cancel: Arc<AtomicBool>,
|
||||
@@ -1434,18 +1535,45 @@ struct Queue {
|
||||
seq: u64,
|
||||
/// The playing preview, if any: its prefetch renders nearest first.
|
||||
playhead: Option<Playhead>,
|
||||
/// How many lanes there are, and the ones not rendering. A lane is one thread that renders
|
||||
/// one frame at a time. The frames of a playing preview and the frames nobody waits for
|
||||
/// (Cache Frames When Idle) render on the lanes; the pool keeps the viewer's frame and the
|
||||
/// few frames prefetched around it while paused. On the pool a frame's parallel work is
|
||||
/// shared: a thread that took a piece of one frame and waited inside it started rendering
|
||||
/// the queued frames on top, and went back to its piece only once the queue was empty. The
|
||||
/// frames a preview started first, the ones right ahead of the playhead, were done last:
|
||||
/// the preview stood still on a gap while the frames further ahead turned green. Idle
|
||||
/// caching left the same gaps, and held up the viewer's frame when it shared a piece.
|
||||
lanes: usize,
|
||||
idle_lanes: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Queue {
|
||||
/// Whether `j` renders on a lane (else on the pool).
|
||||
fn on_lane(&self, j: &Job) -> bool {
|
||||
self.lanes > 0 && !j.urgent && (j.background || self.playhead.is_some())
|
||||
}
|
||||
|
||||
/// The job a lane (`lane`) or the pool renders next: the [`Queue::best`] of its own.
|
||||
fn next(&self, lane: bool) -> Option<usize> {
|
||||
self.best_of(|j| self.on_lane(j) == lane)
|
||||
}
|
||||
|
||||
/// The job that matters most: urgent first (newest), then, while a preview plays, the
|
||||
/// prefetch frame it reaches first, then prefetch in request order. In request order alone,
|
||||
/// frames asked for again after an eviction (the ones right ahead of the playhead) waited
|
||||
/// behind every frame further ahead (#595).
|
||||
fn best(&self) -> Option<usize> {
|
||||
self.best_of(|_| true)
|
||||
}
|
||||
|
||||
/// [`Queue::best`] among the jobs `of` accepts.
|
||||
fn best_of(&self, of: impl Fn(&Job) -> bool) -> Option<usize> {
|
||||
let p = self.playhead.as_ref();
|
||||
self.jobs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, j)| of(j))
|
||||
.max_by_key(|(_, j)| {
|
||||
let near = if j.urgent { None } else { p.and_then(|p| p.ahead(&j.key)) };
|
||||
(j.urgent, near.is_some(), near.map_or(0, |d| -d), if j.urgent { j.seq as i64 } else { -(j.seq as i64) })
|
||||
|
||||
@@ -437,7 +437,9 @@ fn a_newer_viewer_frame_drops_the_ones_still_waiting() {
|
||||
gpu_display: false,
|
||||
disk: None,
|
||||
};
|
||||
let request = |k: FrameKey, urgent: bool| f.request_with(&src, k, ItemId(k.comp), Tick::ZERO, RenderOpts::default(), urgent);
|
||||
let request = |k: FrameKey, urgent: bool| {
|
||||
f.request_with(&src, k, ItemId(k.comp), Tick::ZERO, RenderOpts::default(), if urgent { Want::Viewer } else { Want::Prefetch })
|
||||
};
|
||||
let other = FrameKey { comp: 2, ..key(1, 0, 0) };
|
||||
request(other, true);
|
||||
request(key(1, 20, 0), false);
|
||||
@@ -558,6 +560,131 @@ fn prefetch_renders_the_frames_the_playhead_reaches_first() {
|
||||
assert_eq!(f.inflight(), 6, "frame 5, behind the playhead, is no longer in flight");
|
||||
}
|
||||
|
||||
/// A decoder that works in parallel, as the renderer does, in a comp that shows it.
|
||||
#[derive(Default)]
|
||||
struct ParallelClip {
|
||||
started: std::sync::atomic::AtomicUsize,
|
||||
/// Frames started by the time frame 1 was done.
|
||||
started_at_first: std::sync::atomic::AtomicUsize,
|
||||
}
|
||||
|
||||
impl FootageSource for ParallelClip {
|
||||
fn frame(&self, _: ItemId, _: &effectcraft_engine::project::Footage, t: Tick) -> Option<Arc<effectcraft_engine::render::Image>> {
|
||||
self.started.fetch_add(1, Ordering::SeqCst);
|
||||
(0..16).into_par_iter().for_each(|_| (0..4).into_par_iter().for_each(|_| std::thread::sleep(std::time::Duration::from_millis(1))));
|
||||
if effectcraft_engine::time::FrameRate::FPS_30.frame_at(t) == 1 {
|
||||
self.started_at_first.store(self.started.load(Ordering::SeqCst), Ordering::SeqCst);
|
||||
}
|
||||
Some(Arc::new(effectcraft_engine::render::Image::new(4, 4)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A 10 s comp of a [`ParallelClip`]: the frame cache, the clip, the render source, the comp
|
||||
/// and the key of its frame 0.
|
||||
fn parallel_clip() -> (Frames, Arc<ParallelClip>, RenderSource, ItemId, FrameKey) {
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::project::{Footage, FootageKind};
|
||||
use effectcraft_engine::time::FrameRate;
|
||||
use serde_json::json;
|
||||
let mut s = Session::default();
|
||||
let cid = ItemId(s.execute("comp.new", json!({"name": "A", "width": 8, "height": 8, "duration": 10})).unwrap()["comp"].as_u64().unwrap());
|
||||
let clip = Footage {
|
||||
kind: FootageKind::Video,
|
||||
width: 4,
|
||||
height: 4,
|
||||
has_video: true,
|
||||
frame_rate: FrameRate::FPS_30,
|
||||
duration: Tick::from_seconds_f64(10.0),
|
||||
..Default::default()
|
||||
};
|
||||
let item = Arc::make_mut(&mut s.project).add_item("clip.mov", Default::default(), None, ItemKind::Footage(clip));
|
||||
s.execute("layer.addItem", json!({"item": item.0})).unwrap();
|
||||
let footage = Arc::new(ParallelClip::default());
|
||||
let src = RenderSource {
|
||||
project: s.project.clone(),
|
||||
footage: footage.clone(),
|
||||
expr: None,
|
||||
layer_cache: Arc::default(),
|
||||
gpu: None,
|
||||
gpu_display: false,
|
||||
disk: None,
|
||||
};
|
||||
let f = Frames::default();
|
||||
let series = FrameKey { revision: s.revision, content: f.content_of(&s.project, s.revision, cid), comp: cid.0, frame: 0, scale: 1000, view: 0, opts: 0 };
|
||||
(f, footage, src, cid, series)
|
||||
}
|
||||
|
||||
fn wait_for_renders(f: &Frames) {
|
||||
for _ in 0..8000 {
|
||||
if f.inflight() == 0 {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
assert_eq!(f.inflight(), 0, "every queued frame rendered");
|
||||
}
|
||||
|
||||
/// A playing preview's frames render side by side, and each works in parallel: a render
|
||||
/// waiting for its own parallel work took over pieces of the frames started after it, and
|
||||
/// its frame was done only once theirs were. The frames right ahead of the playhead, started
|
||||
/// first, came last: the preview stood still while the frames further ahead turned green.
|
||||
/// Cache Frames When Idle left the same gaps.
|
||||
#[test]
|
||||
fn frames_started_first_are_done_first() {
|
||||
use effectcraft_engine::time::FrameRate;
|
||||
for playing in [true, false] {
|
||||
let (f, footage, src, cid, series) = parallel_clip();
|
||||
let frames = 60;
|
||||
if playing {
|
||||
f.set_playhead(Some(Playhead { series, cur: 0, start: 0, end: 299, step: 1, looping: false }));
|
||||
}
|
||||
for frame in 1..=frames {
|
||||
let (key, t) = (FrameKey { frame, ..series }, FrameRate::FPS_30.tick_of(frame));
|
||||
if playing {
|
||||
f.request(&src, key, cid, t, RenderOpts::default());
|
||||
} else {
|
||||
f.request_background(&src, key, cid, t, RenderOpts::default());
|
||||
}
|
||||
if frame == 1 {
|
||||
// Its parallel work has started (and idle threads have taken pieces of it).
|
||||
std::thread::sleep(std::time::Duration::from_millis(3));
|
||||
}
|
||||
}
|
||||
wait_for_renders(&f);
|
||||
assert_eq!(f.cached_frames(&series).len(), frames as usize);
|
||||
let before = footage.started_at_first.load(Ordering::SeqCst);
|
||||
assert!(before <= 2 * f.parallelism(), "{before} frames were started before the first one was done (playing: {playing})");
|
||||
}
|
||||
}
|
||||
|
||||
/// Prefetch frames queued when a preview starts or stops, or that become the viewer's
|
||||
/// frame, still render: they change hands between the lanes and the pool.
|
||||
#[test]
|
||||
fn queued_frames_render_when_a_preview_starts_or_stops() {
|
||||
use effectcraft_engine::time::FrameRate;
|
||||
let (f, _, src, cid, series) = parallel_clip();
|
||||
let playhead = Playhead { series, cur: 0, start: 0, end: 299, step: 1, looping: false };
|
||||
let request = |frames: std::ops::Range<i64>| {
|
||||
for frame in frames {
|
||||
f.request(&src, FrameKey { frame, ..series }, cid, FrameRate::FPS_30.tick_of(frame), RenderOpts::default());
|
||||
}
|
||||
};
|
||||
// Queued for the pool, then a preview starts.
|
||||
request(1..40);
|
||||
f.set_playhead(Some(playhead));
|
||||
// Queued for the lanes, then the preview stops.
|
||||
request(40..80);
|
||||
f.set_playhead(None);
|
||||
wait_for_renders(&f);
|
||||
// Queued for the lanes, then the viewer shows one of them.
|
||||
for frame in 80..120 {
|
||||
f.request_background(&src, FrameKey { frame, ..series }, cid, FrameRate::FPS_30.tick_of(frame), RenderOpts::default());
|
||||
}
|
||||
f.request_urgent(&src, FrameKey { frame: 119, ..series }, cid, FrameRate::FPS_30.tick_of(119), RenderOpts::default());
|
||||
wait_for_renders(&f);
|
||||
assert_eq!(f.cached_frames(&series).len(), 119);
|
||||
}
|
||||
|
||||
/// The cache is full. A zoom (or preview-resolution) change left the frames of the old
|
||||
/// size to the plain least-recently-shown order, so the frames of the zoom the viewer is back
|
||||
/// at went before the ones of the abandoned zoom. The other size's frames go first, then the
|
||||
|
||||
Reference in New Issue
Block a user