mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 10:36:56 -06:00
- GPU in browser job workers: each job worker opens its own WebGPU device (deferred readbacks); the Render Queue export and the analyses' frame loops are futures (render::passes, offload::run_request_async) whose frames render in passes, awaiting the device between them. GPU particles and Advanced 3D read back under keys too. workers.jobs reports passes and readbacks; the smoke test measures them. - Layer buffers in the browser's OPFS disk cache: frame workers back their layer caches with a PrefetchStore; the page plans what to fetch before a request from recent misses (LayerPrefetch over the shared DiskIndex). - Templates embed footage (default on, size-capped with a warning) and creating from a template extracts it next to the new project / into browser storage. - ICC LUT profiles (lut8/lut16/lutAtoB/lutBtoA, from the public ICC spec) in Simulate Output > My Custom RGB, baked into a 3D LUT for the viewer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
4.2 KiB
Rust
105 lines
4.2 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 }
|
|
}
|
|
|
|
/// 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.
|
|
pub async fn comp_frame(r: &Renderer<'_>, comp: ItemId, t: Tick) -> Image {
|
|
let Some(a) = r.accel.filter(|a| a.miss_gate().is_some()) else { 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()
|
|
{
|
|
let key = AutoKey::new(comp, opts.scale, false);
|
|
let gpu = pick.choose(key);
|
|
opts.backend = if gpu { Backend::Gpu } else { Backend::Cpu };
|
|
auto = Some((key, gpu));
|
|
}
|
|
}
|
|
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((key, chose)), Some(pick)) = (auto, a.auto_pick()) {
|
|
pick.record(key, chose, 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();
|
|
}
|
|
}
|