Files
effectcraft/crates/render/src/passes.rs
T
nazar-gavrylyk db21c733b4 Auto: a frame's ticket ends it on every exit
AutoPick::ticket chooses a side and reports the frame's time once; dropped without one it ends the frame unmeasured. The viewer's frames, the renderer's top-level frames and frames rendered in passes use it, so an Auto CPU frame turned away by GPU admission (#3028), a cancelled export frame or any later exit no longer leaves its frame counted as running. The frame server keeps its choice across calls and still ends a replaced frame itself.
2026-11-10 15:07:57 +01:00

119 lines
4.7 KiB
Rust

//! Rendering with deferred readbacks from loops that are not frame servers (the browser's
//! job workers: Render Queue renders and analyses).
//!
//! A frame worker renders one frame per request and resumes it pass by pass
//! (`FrameServer::resume`). Jobs render many frames from a loop; written as `async` functions,
//! the loop awaits [`in_passes`] for each frame: with an accelerator whose readbacks are
//! deferred ([`Accelerator::miss_gate`]), the render runs again after every pass that missed,
//! awaiting the device in between ([`Accelerator::settle`]), until a pass completes; with any
//! other accelerator (or none) it runs once and the future is ready at once. Desktop threads
//! drive the same futures with [`block_on`].
use std::future::Future;
use std::task::{Context, Poll, Waker};
use effectcraft_project::ItemId;
use effectcraft_raster::Image;
use effectcraft_time::Tick;
use crate::{Accelerator, AutoKey, Backend, Renderer};
/// Passes before a frame gives up on deferred readbacks and renders on the CPU.
pub const MAX_PASSES: u32 = 24;
/// What [`in_passes`] returns.
#[derive(Clone, Debug)]
pub struct Passed<T> {
pub value: T,
/// Render passes (1 without deferred readbacks).
pub passes: u32,
/// Rendered with the accelerator (not the CPU fallback after [`MAX_PASSES`]).
pub accelerated: bool,
}
/// Whether `accel` defers its readbacks (renders take passes).
pub fn is_deferred(accel: Option<&dyn Accelerator>) -> bool {
accel.is_some_and(|a| a.miss_gate().is_some())
}
/// Run `render` (one render with the accelerator it is given) until a pass doesn't miss: see
/// the module docs. After [`MAX_PASSES`] it renders once without the accelerator (the CPU).
/// Caches the render writes to must hold the accelerator's miss gate
/// ([`crate::LayerCache::set_gate`]).
pub async fn in_passes<'a, T>(accel: Option<&'a dyn Accelerator>, mut render: impl FnMut(Option<&'a dyn Accelerator>) -> T) -> Passed<T> {
let Some(a) = accel.filter(|a| a.miss_gate().is_some()) else {
return Passed { value: render(accel), passes: 1, accelerated: accel.is_some() };
};
a.frame_begin();
for pass in 1..=MAX_PASSES {
a.pass_begin();
let value = render(Some(a));
if !a.pass_missed() {
return Passed { value, passes: pass, accelerated: true };
}
match a.settle() {
Some(s) => s.await,
None => break,
}
}
Passed { value: render(None), passes: MAX_PASSES + 1, accelerated: false }
}
/// Ends the frame for the accelerator ([`Accelerator::frame_end`]) when dropped: also when the
/// render panics or the future is dropped between passes.
struct FrameEnd<'a>(&'a dyn Accelerator);
impl Drop for FrameEnd<'_> {
fn drop(&mut self) {
self.0.frame_end();
}
}
/// A top-level comp frame through [`in_passes`] ([`Renderer::comp_frame`] in passes). Backend
/// Auto picks CPU or GPU for the whole frame (all its passes) and is timed as a whole, as the
/// frame workers do. Ends the frame for the accelerator ([`Accelerator::frame_end`]) on every
/// exit, including a panic.
pub async fn comp_frame(r: &Renderer<'_>, comp: ItemId, t: Tick) -> Image {
let Some(a) = r.accel else { return r.comp_frame(comp, t) };
let _end = FrameEnd(a);
if a.miss_gate().is_none() {
return r.comp_frame(comp, t);
}
let mut opts = r.opts;
let mut auto = None;
if opts.backend == Backend::Auto {
if !r.project.settings.gpu_acceleration {
opts.backend = Backend::Cpu;
} else if opts.roi.is_none()
&& let Some(pick) = a.auto_pick()
{
// (a frame cancelled or panicking between passes drops it: ended unmeasured)
let ticket = pick.ticket(AutoKey::new(comp, opts.scale, false));
opts.backend = if ticket.gpu() { Backend::Gpu } else { Backend::Cpu };
auto = Some(ticket);
}
}
let t0 = web_time::Instant::now();
let img = if opts.backend == Backend::Cpu {
Renderer { opts, accel: None, ..*r }.comp_frame(comp, t)
} else {
in_passes(Some(a), |acc| Renderer { opts, accel: acc, ..*r }.comp_frame(comp, t)).await.value
};
if let Some(t) = auto {
t.record(t0.elapsed().as_secs_f64() * 1e3);
}
img
}
/// Drive `f` to completion on this thread. For futures that only wait on deferred readbacks,
/// whose [`Accelerator::settle`] blocks natively: they never stay pending there.
pub fn block_on<F: Future>(f: F) -> F::Output {
let mut f = std::pin::pin!(f);
let mut cx = Context::from_waker(Waker::noop());
loop {
if let Poll::Ready(v) = f.as_mut().poll(&mut cx) {
return v;
}
std::thread::yield_now();
}
}