effectcraft/crates/ui-egui/src/frames.rs

695 lines
27 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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 (project
//! revision, comp, frame, scale); the timeline draws the cached range as the green cache bar.
//!
//! wasm32 has no threads: jobs wait in the same priority queue and [`Frames::pump`] hands them
//! to a [`RemoteFrames`] (the browser's frame worker, a second engine instance fed with project
//! diffs, `effectcraft_engine::remote`) or, without one, renders them on the UI thread between
//! egui frames, within a time budget.
//!
//! With the disk cache on (Settings ▸ Media & Disk Cache), every CPU-rendered frame is also
//! written to disk under a content key (the comp's content hash, frame, scale, view and render
//! options), and a job first looks there: frames survive restarts and project reopenings. The
//! timeline shows disk-only frames as a blue cache bar.
//!
//! With Mercury GPU Acceleration (a [`Gpu`] on egui-wgpu's device and the project's renderer set
//! to the GPU) frames stay on the GPU: the compositor writes an RGBA8 texture that the viewer
//! registers with egui-wgpu and draws directly; pixels are read back only when something needs
//! them (Info panel, eyedroppers, histograms).
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, Mutex};
use effectcraft_engine::project::{ItemId, Project};
use effectcraft_engine::render::disk_cache::{self, DiskCache};
use effectcraft_engine::render::{ExprHost, FootageSource, LayerCache, RenderOpts, Renderer};
use effectcraft_engine::time::Tick;
use effectcraft_gpu::{DisplayFrame, Gpu};
use rayon::prelude::*;
/// GPU frames kept for RAM preview (bytes of video memory).
const GPU_BUDGET: usize = 1 << 30;
/// A rendered viewer frame: CPU pixels or a GPU texture.
#[derive(Clone)]
pub enum FrameImage {
Cpu(Arc<egui::ColorImage>),
Gpu(Arc<DisplayFrame>),
}
impl FrameImage {
pub fn size(&self) -> [usize; 2] {
match self {
FrameImage::Cpu(c) => c.size,
FrameImage::Gpu(f) => [f.width as usize, f.height as usize],
}
}
fn bytes(&self) -> usize {
self.size()[0] * self.size()[1] * 4
}
fn is_gpu(&self) -> bool {
matches!(self, FrameImage::Gpu(_))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FrameKey {
pub revision: u64,
pub comp: u64,
pub frame: i64,
/// Render scale × 1000.
pub scale: u32,
/// Hash of the 3D view camera (0 = the comp's active camera).
pub view: u64,
}
struct Cache {
map: HashMap<FrameKey, FrameImage>,
order: VecDeque<FrameKey>,
bytes: usize,
/// Bytes of GPU frames (also counted in `bytes`).
gpu_bytes: usize,
budget: usize,
}
impl Cache {
fn insert(&mut self, k: FrameKey, img: FrameImage) {
let (sz, gpu) = (img.bytes(), img.is_gpu());
if let Some(old) = self.map.insert(k, img) {
self.forget(&old);
} else {
self.order.push_back(k);
}
self.bytes += sz;
if gpu {
self.gpu_bytes += sz;
}
while self.bytes > self.budget {
let Some(old) = self.order.pop_front() else { break };
self.remove(&old);
}
// Video memory: drop the oldest GPU frames past their own budget.
while self.gpu_bytes > GPU_BUDGET {
let Some(i) = self.order.iter().position(|k| self.map.get(k).is_some_and(FrameImage::is_gpu)) else { break };
if let Some(old) = self.order.remove(i) {
self.remove(&old);
}
}
}
fn forget(&mut self, img: &FrameImage) {
self.bytes -= img.bytes();
if img.is_gpu() {
self.gpu_bytes -= img.bytes();
}
}
fn remove(&mut self, k: &FrameKey) {
if let Some(i) = self.map.remove(k) {
self.forget(&i);
}
}
fn evict_to_budget(&mut self) {
while self.bytes > self.budget {
let Some(old) = self.order.pop_front() else { break };
self.remove(&old);
}
}
}
/// What a render job needs (all cheap clones).
#[derive(Clone)]
pub struct RenderSource {
pub project: Arc<Project>,
pub footage: Arc<dyn FootageSource>,
pub expr: Option<Arc<dyn ExprHost>>,
pub layer_cache: Arc<LayerCache>,
/// GPU compositor on the viewer's device (Mercury GPU Acceleration), if available.
pub gpu: Option<Gpu>,
/// Keep GPU frames on the GPU for the viewer (native egui-wgpu textures). Off on the
/// desktop: Show Channel / exposure / region-of-interest drawing read the CPU frame texture,
/// so GPU frames are read back (the compositing still runs on the GPU). On in the browser
/// while the viewer draws plainly, because there frames can't be read back synchronously
/// (`EffectcraftApp::gpu_display`).
pub gpu_display: bool,
/// The persistent disk cache, when enabled.
pub disk: Option<Arc<DiskCache>>,
}
/// Content keys of (project revision, comp), computed once per revision.
type ContentKeys = Arc<Mutex<HashMap<(u64, u64), u128>>>;
fn content_key(keys: &ContentKeys, project: &Project, revision: u64, comp: u64) -> u128 {
if let Some(k) = keys.lock().ok().and_then(|m| m.get(&(revision, comp)).copied()) {
return k;
}
let k = disk_cache::comp_content_key(project, ItemId(comp));
if let Ok(mut m) = keys.lock() {
if m.len() > 256 {
m.clear();
}
m.insert((revision, comp), k);
}
k
}
fn opts_hash(o: &RenderOpts) -> u64 {
let mut h = disk_cache::Hash128::default();
h.write(format!("{o:?}").as_bytes());
h.finish() as u64
}
/// Disk key of a viewer frame (render options included: region of interest, draft…).
fn frame_disk_key(keys: &ContentKeys, project: &Project, key: &FrameKey, opts_hash: u64) -> u128 {
let content = content_key(keys, project, key.revision, key.comp);
disk_cache::frame_key(content, key.frame, key.scale, key.view ^ opts_hash)
}
/// A frame for a [`RemoteFrames`] renderer.
pub struct RemoteJob {
pub project: Arc<Project>,
pub revision: u64,
pub comp: ItemId,
pub t: Tick,
pub opts: RenderOpts,
/// The frame's disk-cache key, when the renderer keeps a disk cache
/// ([`RemoteFrames::disk`]): served from it on a hit, stored there after a render.
pub disk_key: Option<u128>,
}
/// Premultiplied RGBA8 pixels from a [`RemoteFrames`] renderer.
pub struct RemoteFrame {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
/// Render time (ms).
pub ms: f64,
}
/// Called once with a remote frame (or why there is none).
pub type RemoteDone = Box<dyn FnOnce(Result<RemoteFrame, String>) + Send>;
/// Renders viewer frames elsewhere (the browser's frame worker), so the UI thread never does.
pub trait RemoteFrames: Send + Sync {
/// Frames it can render at the same time (0 when it is not usable: frames then render on
/// the UI thread again).
fn slots(&self) -> usize;
/// Start rendering `job`; `done` runs when the frame arrives.
fn start(&self, job: RemoteJob, done: RemoteDone);
/// Drop cached intermediate results (Edit ▸ Purge).
fn purge(&self) {}
/// It renders GPU effects (its own GPU device): frames with effects go to it rather than
/// the UI thread's GPU compositor, which would run their effects on the CPU.
fn gpu(&self) -> bool {
false
}
/// It keeps a disk cache of frames (the browser's Origin Private File System).
fn disk(&self) -> bool {
false
}
/// The disk cache holds the frame of this key (the blue cache bar).
fn disk_contains(&self, _key: u128) -> bool {
false
}
}
/// Whether a comp (or a comp it nests) has a layer with enabled effects.
pub fn comp_has_effects(project: &Project, comp: ItemId) -> bool {
fn walk(p: &Project, comp: ItemId, depth: usize) -> bool {
let Some(c) = p.comp(comp) else { return false };
c.layers.iter().any(|l| {
l.switches.effects && l.effects().is_some_and(|fx| fx.groups().any(|g| g.enabled))
|| depth < 16 && matches!(l.source, effectcraft_engine::project::LayerSource::Comp { item } if walk(p, item, depth + 1))
})
}
walk(project, comp, 0)
}
pub struct Frames {
cache: Arc<Mutex<Cache>>,
/// Keys queued or rendering.
inflight: Arc<Mutex<HashSet<FrameKey>>>,
/// Pending jobs, picked by priority when a pool thread frees up (the viewer's frame first).
queue: Arc<Mutex<Queue>>,
#[cfg(not(target_arch = "wasm32"))]
pool: 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>>,
content_keys: ContentKeys,
/// Where frames render instead of the UI thread (wasm32), and how many it is rendering.
remote: Option<Arc<dyn RemoteFrames>>,
remote_busy: Arc<Mutex<usize>>,
}
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);
Frames {
cache: Arc::new(Mutex::new(Cache { map: HashMap::new(), order: VecDeque::new(), bytes: 0, gpu_bytes: 0, budget: 3 << 30 })),
inflight: Arc::new(Mutex::new(HashSet::new())),
queue: Arc::new(Mutex::new(Queue::default())),
#[cfg(not(target_arch = "wasm32"))]
pool: rayon::ThreadPoolBuilder::new().num_threads(threads).thread_name(|i| format!("ec-frame-{i}")).build().expect("frame pool"),
ctx: None,
last_ms: Arc::new(Mutex::new(0.0)),
content_keys: Arc::default(),
remote: None,
remote_busy: Arc::default(),
}
}
}
/// Premultiplied f32 → egui premultiplied Color32 (alpha kept, so the viewer can show the
/// transparency grid or the comp background colour underneath).
pub fn to_color_image(img: &effectcraft_engine::render::Image) -> egui::ColorImage {
let px: Vec<egui::Color32> = img
.data
.par_iter()
.map(|p| {
let a = p[3].clamp(0.0, 1.0);
let c = |v: f32| (v.clamp(0.0, a) * 255.0 + 0.5) as u8;
egui::Color32::from_rgba_premultiplied(c(p[0]), c(p[1]), c(p[2]), (a * 255.0 + 0.5) as u8)
})
.collect();
egui::ColorImage::new([img.width as usize, img.height as usize], px)
}
impl Frames {
pub fn set_context(&mut self, ctx: &egui::Context) {
if self.ctx.is_none() {
self.ctx = Some(ctx.clone());
}
}
/// The egui context (to request repaints from callbacks).
pub fn context(&self) -> Option<egui::Context> {
self.ctx.clone()
}
pub fn get(&self, k: &FrameKey) -> Option<FrameImage> {
self.cache.lock().ok()?.map.get(k).cloned()
}
pub fn is_cached(&self, k: &FrameKey) -> bool {
self.cache.lock().map(|c| c.map.contains_key(k)).unwrap_or(false)
}
pub fn inflight(&self) -> usize {
self.inflight.lock().map(|s| s.len()).unwrap_or(0)
}
/// Cached frame numbers for (revision, comp, scale) — for the cache bar.
pub fn cached_frames(&self, revision: u64, comp: u64, scale: u32) -> Vec<i64> {
let Ok(c) = self.cache.lock() else { return vec![] };
let mut v: Vec<i64> = c.map.keys().filter(|k| k.revision == revision && k.comp == comp && k.scale == scale).map(|k| k.frame).collect();
v.sort_unstable();
v
}
/// Frames of (revision, comp, scale) in the disk cache but not in RAM — the blue cache bar.
/// `opts` are the viewer's render options (part of the disk key).
pub fn disk_frames(&self, src: &RenderSource, revision: u64, comp: u64, scale: u32, view: u64, frames: i64, opts: &RenderOpts) -> Vec<i64> {
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),
(None, Some(r)) => r.disk_contains(k),
(None, None) => false,
};
if src.disk.is_none() && remote.is_none() {
return vec![];
}
let ram: HashSet<i64> = self.cached_frames(revision, comp, scale).into_iter().collect();
let oh = opts_hash(opts);
(0..frames.clamp(0, 100_000))
.filter(|f| !ram.contains(f))
.filter(|f| {
let key = FrameKey { revision, comp, frame: *f, scale, view };
contains(frame_disk_key(&self.content_keys, &src.project, &key, oh))
})
.collect()
}
/// Change the RAM preview cache budget (Settings ▸ Memory & CPU), evicting the oldest
/// frames when it shrinks.
pub fn set_budget(&self, bytes: usize) {
if let Ok(mut c) = self.cache.lock() {
c.budget = bytes;
c.evict_to_budget();
}
}
pub fn budget(&self) -> usize {
self.cache.lock().map(|c| c.budget).unwrap_or(0)
}
pub fn clear(&self) {
if let Some(r) = &self.remote {
r.purge();
}
if let Ok(mut c) = self.cache.lock() {
c.map.clear();
c.order.clear();
c.bytes = 0;
c.gpu_bytes = 0;
}
}
/// 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);
}
/// 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);
}
/// An urgent (viewer) frame is queued or rendering.
pub fn urgent_pending(&self) -> bool {
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) {
if self.is_cached(&key) {
return;
}
let Ok(mut q) = self.queue.lock() else { return };
// Jobs for an older project revision can never be shown: drop them.
let before = q.jobs.len();
q.jobs.retain(|j| j.key.revision >= key.revision);
let dropped = before - q.jobs.len();
if dropped > 0
&& let Ok(mut inf) = self.inflight.lock()
{
inf.retain(|k| k.revision >= key.revision || q.running.contains(k));
}
if urgent {
// Only the newest viewer frame is urgent; earlier ones become prefetch.
for j in q.jobs.iter_mut() {
j.urgent = j.key == key;
}
}
{
let Ok(mut inf) = self.inflight.lock() else { return };
if !inf.insert(key) {
return;
}
}
q.seq += 1;
let seq = q.seq;
q.jobs.push(Job { key, src: src.clone(), comp, t, opts, urgent, seq });
drop(q);
#[cfg(not(target_arch = "wasm32"))]
if self.remote.is_none() {
let w = self.worker();
// One spawn per job; each spawn renders whichever queued job matters most right now.
self.pool.spawn(move || {
w.run_next();
});
}
#[cfg(target_arch = "wasm32")]
if let Some(ctx) = &self.ctx {
// rendered by `pump` after this UI frame
ctx.request_repaint();
}
}
fn worker(&self) -> Worker {
Worker {
queue: self.queue.clone(),
cache: self.cache.clone(),
inflight: self.inflight.clone(),
ctx: self.ctx.clone(),
last: self.last_ms.clone(),
content_keys: self.content_keys.clone(),
}
}
/// Render frames with `remote` from now on (`None`: on this thread again).
pub fn set_remote(&mut self, remote: Option<Arc<dyn RemoteFrames>>) {
self.remote = remote;
}
/// The remote renderer keeps a disk cache (the browser's).
pub fn remote_disk(&self) -> bool {
self.remote.as_ref().is_some_and(|r| r.disk())
}
/// Whether frames go to a usable [`RemoteFrames`].
pub fn remote_active(&self) -> bool {
self.remote.as_ref().is_some_and(|r| r.slots() > 0)
}
/// Frames the remote renderer renders at once (0 without one).
pub fn remote_slots(&self) -> usize {
self.remote.as_ref().map(|r| r.slots()).unwrap_or(0)
}
/// Frames being rendered remotely.
pub fn remote_busy(&self) -> usize {
self.remote_busy.lock().map(|b| *b).unwrap_or(0)
}
/// Hand queued frames to the remote renderer while it has free slots, most important first
/// (the viewer's frame, then prefetch in request order; a viewer frame requested while every
/// slot is busy goes to the first one that frees up). Frames that the GPU compositor can keep
/// on the GPU render here instead (no readback). Returns how many were started.
pub fn dispatch_remote(&self) -> usize {
let Some(remote) = self.remote.clone() else { return 0 };
let slots = remote.slots();
let w = self.worker();
let mut started = 0;
loop {
let busy = self.remote_busy();
if busy >= slots {
break;
}
let job = {
let Ok(mut q) = self.queue.lock() else { break };
let Some(i) = Queue::best(&q.jobs) else { break };
let job = q.jobs.swap_remove(i);
q.running.insert(job.key);
if job.urgent {
q.urgent_running += 1;
}
job
};
// The GPU compositor here keeps the frame on the GPU, but runs layer effects on the
// CPU (this thread can't wait for readbacks): frames with effects go to workers
// that render them on their own GPU.
let gpu_here = job.src.gpu.is_some() && job.src.gpu_display && !(remote.gpu() && comp_has_effects(&job.src.project, job.comp));
if gpu_here {
let t0 = web_time::Instant::now();
if let Some(img) = w.render_gpu(&job) {
w.finish(&job, img, t0);
continue;
}
}
if let Ok(mut b) = self.remote_busy.lock() {
*b += 1;
}
started += 1;
let (key, urgent) = (job.key, job.urgent);
let fw = self.worker();
let busy = self.remote_busy.clone();
let t0 = web_time::Instant::now();
let disk_key = remote.disk().then(|| frame_disk_key(&w.content_keys, &job.src.project, &key, opts_hash(&job.opts)));
let rj = RemoteJob { project: job.src.project.clone(), revision: key.revision, comp: job.comp, t: job.t, opts: job.opts, disk_key };
remote.start(
rj,
Box::new(move |r: Result<RemoteFrame, String>| {
if let Ok(mut b) = busy.lock() {
*b = b.saturating_sub(1);
}
match r {
Ok(f) => {
let img = egui::ColorImage::from_rgba_premultiplied([f.width as usize, f.height as usize], &f.rgba);
fw.finish_key(key, urgent, FrameImage::Cpu(Arc::new(img)), t0);
}
Err(e) => {
// Not rendered (a stale revision, a lost worker): the viewer asks again.
log::debug!("remote frame: {e}");
fw.release(key, urgent);
}
}
}),
);
}
started
}
/// Render queued frames, most important first. With a usable [`RemoteFrames`] they are only
/// handed over ([`Frames::dispatch_remote`]); otherwise they render on this thread until the
/// queue is empty or `budget` has passed (at least one frame). Returns whether frames are
/// still queued or rendering remotely. Only wasm32 needs this (native frames render on the
/// pool); elsewhere it does nothing.
pub fn pump(&self, budget: std::time::Duration) -> bool {
if cfg!(not(target_arch = "wasm32")) {
return false;
}
if self.remote_active() {
self.dispatch_remote();
return self.queue.lock().map(|q| !q.jobs.is_empty()).unwrap_or(false) || self.remote_busy() > 0;
}
let t0 = web_time::Instant::now();
let w = self.worker();
while w.run_next() {
if t0.elapsed() >= budget {
break;
}
}
self.queue.lock().map(|q| !q.jobs.is_empty()).unwrap_or(false)
}
}
/// Renders queued jobs (on a pool thread, or the UI thread on wasm32).
struct Worker {
queue: Arc<Mutex<Queue>>,
cache: Arc<Mutex<Cache>>,
inflight: Arc<Mutex<HashSet<FrameKey>>>,
ctx: Option<egui::Context>,
last: Arc<Mutex<f64>>,
content_keys: ContentKeys,
}
impl Worker {
/// Render the queued job that matters most right now. `false` when the queue was empty.
fn run_next(&self) -> bool {
let Worker { queue, content_keys, .. } = self;
let job = {
let Ok(mut q) = queue.lock() else { return false };
let Some(i) = Queue::best(&q.jobs) else { return false };
let job = q.jobs.swap_remove(i);
q.running.insert(job.key);
if job.urgent {
q.urgent_running += 1;
}
job
};
let t0 = web_time::Instant::now();
let disk = job.src.disk.as_ref().map(|dc| (dc.clone(), frame_disk_key(content_keys, &job.src.project, &job.key, opts_hash(&job.opts))));
let from_disk = disk.as_ref().and_then(|(dc, k)| dc.get_frame(*k));
let ci = match from_disk {
Some(f) => FrameImage::Cpu(Arc::new(egui::ColorImage::from_rgba_premultiplied([f.width as usize, f.height as usize], &f.rgba))),
None => {
let ci = self.render_job(&job);
if let (Some((dc, k)), FrameImage::Cpu(img)) = (&disk, &ci) {
let rgba: Vec<u8> = img.pixels.iter().flat_map(|c| c.to_array()).collect();
dc.put_frame(*k, img.size[0] as u32, img.size[1] as u32, &rgba);
}
ci
}
};
self.finish(&job, ci, t0);
true
}
/// Store a finished frame and release its job.
fn finish(&self, job: &Job, img: FrameImage, t0: web_time::Instant) {
self.finish_key(job.key, job.urgent, img, t0);
}
fn finish_key(&self, key: FrameKey, urgent: bool, img: FrameImage, t0: web_time::Instant) {
if urgent && let Ok(mut l) = self.last.lock() {
*l = t0.elapsed().as_secs_f64() * 1000.0;
}
if let Ok(mut c) = self.cache.lock() {
c.insert(key, img);
}
self.release(key, urgent);
}
/// The job of `key` is no longer queued or running (rendered or dropped).
fn release(&self, key: FrameKey, urgent: bool) {
if let Ok(mut inf) = self.inflight.lock() {
inf.remove(&key);
}
if let Ok(mut q) = self.queue.lock() {
q.running.remove(&key);
if urgent {
q.urgent_running = q.urgent_running.saturating_sub(1);
}
}
if let Some(ctx) = &self.ctx {
ctx.request_repaint();
}
}
/// The frame on the GPU, kept there for the viewer, if the compositor can do it alone.
fn render_gpu(&self, job: &Job) -> Option<FrameImage> {
let g = job.src.gpu.as_ref()?;
let mut r = Renderer::new(&job.src.project, job.src.footage.as_ref(), job.opts);
r.expr = job.src.expr.as_deref();
r.cache = Some(&job.src.layer_cache);
r.accel = Some(g as &dyn effectcraft_engine::render::Accelerator);
r.active_accel()?;
g.render_display(&r, job.comp, job.t).map(|f| FrameImage::Gpu(Arc::new(f)))
}
fn render_job(&self, job: &Job) -> FrameImage {
let mut r = Renderer::new(&job.src.project, job.src.footage.as_ref(), job.opts);
r.expr = job.src.expr.as_deref();
r.cache = Some(&job.src.layer_cache);
r.accel = job.src.gpu.as_ref().map(|g| g as &dyn effectcraft_engine::render::Accelerator);
// The GPU leaves the frame in a texture for the viewer; otherwise (Software Only, no
// adapter, or a frame the GPU cannot finish here) the CPU renders it.
// Auto (Mercury GPU Acceleration) shows each comp on whichever compositor measured
// faster for it (light comps composite faster on the CPU).
if let (Some(g), Some(a)) = (&job.src.gpu, r.active_accel())
&& job.src.gpu_display
{
// The browser cannot wait for the GPU to time it (and its CPU path is slow): always
// the GPU there.
let auto = if cfg!(target_arch = "wasm32") { None } else { r.frame_auto(a, job.comp, true) };
if auto.is_none_or(|(p, key)| p.choose(key)) {
let t0 = web_time::Instant::now();
if let Some(f) = g.render_display(&r, job.comp, job.t) {
if let Some((p, key)) = auto {
// Time the GPU's work, not just its recording.
g.wait();
p.record(key, true, t0.elapsed().as_secs_f64() * 1000.0);
}
return FrameImage::Gpu(Arc::new(f));
}
if let Some((p, key)) = auto {
p.declined(key);
}
}
let t0 = web_time::Instant::now();
let img = to_color_image(&r.comp_frame_cpu(job.comp, job.t));
if let Some((p, key)) = auto {
p.record(key, false, t0.elapsed().as_secs_f64() * 1000.0);
}
return FrameImage::Cpu(Arc::new(img));
}
FrameImage::Cpu(Arc::new(to_color_image(&r.comp_frame(job.comp, job.t))))
}
}
struct Job {
key: FrameKey,
src: RenderSource,
comp: ItemId,
t: Tick,
opts: RenderOpts,
urgent: bool,
seq: u64,
}
#[derive(Default)]
struct Queue {
jobs: Vec<Job>,
running: HashSet<FrameKey>,
urgent_running: usize,
seq: u64,
}
impl Queue {
/// The job that matters most: urgent first (newest), then prefetch in request order.
fn best(jobs: &[Job]) -> Option<usize> {
jobs.iter().enumerate().max_by_key(|(_, j)| (j.urgent, if j.urgent { j.seq as i64 } else { -(j.seq as i64) })).map(|(i, _)| i)
}
}