mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 10:36:56 -06:00
Shape and text layers have comp-sized effect bounds centred on their origin, but effects measured positions from the origin: a point at its default "layer centre" sat at the comp's bottom-right corner, so Twirl on a centred shape changed nothing and Bulge only its bottom-right quarter. As in After Effects, effects now work in effect space, measured from the top-left of the layer's effect bounds. The renderer moves the layer buffer into effect space for the whole stack (Buf::rebase, CPU and GPU alike) and hands effects everything else in it: masks, other layers' pixels for layer parameters (each in its own effect space, so Set Matte and displacement maps see a whole shape layer), the layer's own frames (layer_input, also what Puppet, Roto Brush and the Layer panel read) and the comp camera (CompScene::rebased). Layer bounds are now right for every effect, so pin_rect and EffectEnv::bounds_origin go and layer_rect serves edge pinning too. Adjustment layers still run on the comp below as it is. effect_bounds / EvalCtx::effect_to_comp replace the repeated "comp size when there is no source" code; the viewer's effect point handles and eyedroppers, Puppet pins, the Layer panel (paint, Roto Brush) and the tracker's Corner Pin output use them. Project schema 2: opening a schema 1 project moves positions held by effects on shape and text layers by half the comp size, so set points keep their place (point parameters with keyframes, Paint paths and clone positions, Puppet seeds and pins, Liquify and Roto Brush strokes). Points still at their default become the layer centre; expression controls, adjustment layers and expressions are left as they are (docs/architecture.md). The disk cache's render version is bumped. Tests: default_effect_points_sit_at_the_centre_of_a_shape_layer and layer_parameters_see_a_whole_shape_layer (render, both fail before), effect_points_on_a_shape_layer (GPU vs CPU), opening_a_schema_1_project_moves_shape_layer_effect_points_into_effect_space (engine), and the effects migration tests. Part of #227 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
356 lines
13 KiB
Rust
356 lines
13 KiB
Rust
//! Deferred readbacks: rendering on a device whose readbacks cannot be waited for (WebGPU in a
|
|
//! browser worker: a buffer maps only once the worker returns to its event loop).
|
|
//!
|
|
//! A frame renders in passes (see [`effectcraft_render::Accelerator::frame_begin`]). Every
|
|
//! readback the renderer needs is keyed by what determines its pixels: a GPU effect chain by
|
|
//! its input buffer's pixels and the chain's parameters, the top-level frame by comp, time and
|
|
//! render options. A key seen for the first time records the GPU work, starts an asynchronous
|
|
//! copy (`map_async`) and *misses*: the step returns a placeholder and the pass is marked missed
|
|
//! (raising the gate that keeps placeholder-derived buffers out of the layer cache). The caller
|
|
//! awaits [`Deferred::settled`] and renders again; keys whose bytes arrived are served from
|
|
//! here. A pass without a miss is the finished frame.
|
|
//!
|
|
//! Correctness: a result is only computed from genuine inputs. Pixels a chain reads are part
|
|
//! of its key, and a chain whose kernels read other data through the effect host (other layers,
|
|
//! other times) is not started when that data missed while it ran (the miss counter moved).
|
|
|
|
use std::collections::HashMap;
|
|
use std::future::Future;
|
|
use std::hash::{Hash, Hasher};
|
|
use std::pin::Pin;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::task::{Context, Poll, Waker};
|
|
|
|
use effectcraft_effects::Buf;
|
|
use effectcraft_render::FxStep;
|
|
|
|
/// Bytes read back for one key, with the geometry they describe.
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct Readback {
|
|
pub bytes: Vec<u8>,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
/// Layer buffer offset and scale (chains).
|
|
pub offset: [f64; 2],
|
|
pub scale: f64,
|
|
}
|
|
|
|
/// What [`Deferred::lookup`] knows about a key.
|
|
pub(crate) enum Lookup {
|
|
/// The bytes arrived (`None`: the readback failed; render this step another way).
|
|
Ready(Option<Readback>),
|
|
/// Started in this frame, not arrived yet.
|
|
InFlight,
|
|
/// Never asked for in this frame.
|
|
Absent,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct State {
|
|
ready: HashMap<u128, Option<Readback>>,
|
|
/// Each request owns a unique token, even when a key is reused after cancellation.
|
|
waiting: HashMap<u128, Arc<()>>,
|
|
/// Misses so far in this frame (a counter, so a chain sees misses during its own work).
|
|
misses: u64,
|
|
wakers: Vec<Waker>,
|
|
}
|
|
|
|
/// Deferred readback state of a device (see the module docs).
|
|
#[derive(Default)]
|
|
pub struct Deferred {
|
|
st: Mutex<State>,
|
|
/// Raised from the pass's first miss to its end.
|
|
gate: Arc<AtomicBool>,
|
|
/// Passes and readbacks (diagnostics).
|
|
pub(crate) passes: std::sync::atomic::AtomicU64,
|
|
pub(crate) readbacks: std::sync::atomic::AtomicU64,
|
|
}
|
|
|
|
impl Deferred {
|
|
pub fn frame_begin(&self) {
|
|
let wakers = {
|
|
let mut s = self.st.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
s.ready.clear();
|
|
s.waiting.clear();
|
|
s.misses = 0;
|
|
std::mem::take(&mut s.wakers)
|
|
};
|
|
for w in wakers {
|
|
w.wake();
|
|
}
|
|
self.gate.store(false, Ordering::Relaxed);
|
|
}
|
|
|
|
pub fn pass_begin(&self) {
|
|
self.gate.store(false, Ordering::Relaxed);
|
|
self.passes.fetch_add(1, Ordering::Relaxed);
|
|
}
|
|
|
|
pub fn missed(&self) -> bool {
|
|
self.gate.load(Ordering::Relaxed)
|
|
}
|
|
|
|
pub fn gate(&self) -> Arc<AtomicBool> {
|
|
self.gate.clone()
|
|
}
|
|
|
|
/// Misses so far in this frame.
|
|
pub(crate) fn misses(&self) -> u64 {
|
|
self.st.lock().map(|s| s.misses).unwrap_or(0)
|
|
}
|
|
|
|
/// Mark the pass as missed.
|
|
pub(crate) fn miss(&self) {
|
|
if let Ok(mut s) = self.st.lock() {
|
|
s.misses += 1;
|
|
}
|
|
self.gate.store(true, Ordering::Relaxed);
|
|
}
|
|
|
|
pub(crate) fn lookup(&self, key: u128) -> Lookup {
|
|
let Ok(s) = self.st.lock() else { return Lookup::Ready(None) };
|
|
if let Some(r) = s.ready.get(&key) {
|
|
return Lookup::Ready(r.clone());
|
|
}
|
|
if s.waiting.contains_key(&key) { Lookup::InFlight } else { Lookup::Absent }
|
|
}
|
|
|
|
/// A readback for `key` is being started; returns the callback that stores its bytes.
|
|
pub(crate) fn start(self: &Arc<Self>, key: u128, width: u32, height: u32, offset: [f64; 2], scale: f64) -> impl FnOnce(Option<Vec<u8>>) + 'static {
|
|
let token = Arc::new(());
|
|
self.st.lock().unwrap_or_else(std::sync::PoisonError::into_inner).waiting.insert(key, token.clone());
|
|
self.readbacks.fetch_add(1, Ordering::Relaxed);
|
|
let me = self.clone();
|
|
move |bytes: Option<Vec<u8>>| {
|
|
let wakers = {
|
|
let mut s = me.st.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
// Reject stale completion before touching either results or accounting.
|
|
if !s.waiting.get(&key).is_some_and(|current| Arc::ptr_eq(current, &token)) {
|
|
return;
|
|
}
|
|
s.waiting.remove(&key);
|
|
s.ready.insert(key, bytes.map(|bytes| Readback { bytes, width, height, offset, scale }));
|
|
if s.waiting.is_empty() { std::mem::take(&mut s.wakers) } else { vec![] }
|
|
};
|
|
for w in wakers {
|
|
w.wake();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Cancel pending requests without exposing placeholder pixels as successful results.
|
|
/// Existing waiters wake; late callbacks cannot change a newer request's accounting.
|
|
pub(crate) fn cancel(&self) {
|
|
let wakers = {
|
|
let mut s = self.st.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
for (key, _) in std::mem::take(&mut s.waiting) {
|
|
s.ready.insert(key, None);
|
|
}
|
|
std::mem::take(&mut s.wakers)
|
|
};
|
|
for w in wakers {
|
|
w.wake();
|
|
}
|
|
}
|
|
|
|
/// Readbacks still in flight.
|
|
pub fn in_flight(&self) -> usize {
|
|
self.st.lock().unwrap_or_else(std::sync::PoisonError::into_inner).waiting.len()
|
|
}
|
|
|
|
/// Resolves once no readback is in flight (on the web the browser's event loop delivers
|
|
/// them; natively a device poll does).
|
|
pub fn settled(self: &Arc<Self>) -> Settled {
|
|
Settled(self.clone())
|
|
}
|
|
}
|
|
|
|
/// See [`Deferred::settled`].
|
|
pub struct Settled(Arc<Deferred>);
|
|
|
|
impl Future for Settled {
|
|
type Output = ();
|
|
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
|
|
let mut s = self.0.st.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
if s.waiting.is_empty() {
|
|
return Poll::Ready(());
|
|
}
|
|
if !s.wakers.iter().any(|w| w.will_wake(cx.waker())) {
|
|
s.wakers.push(cx.waker().clone());
|
|
}
|
|
Poll::Pending
|
|
}
|
|
}
|
|
|
|
/// 128-bit key from two differently seeded std hashers (deterministic within a process).
|
|
pub(crate) struct Key(std::collections::hash_map::DefaultHasher, std::collections::hash_map::DefaultHasher);
|
|
|
|
impl Key {
|
|
pub fn new(tag: u64) -> Key {
|
|
let mut a = std::collections::hash_map::DefaultHasher::new();
|
|
let mut b = std::collections::hash_map::DefaultHasher::new();
|
|
tag.hash(&mut a);
|
|
(tag ^ 0x9e37_79b9_7f4a_7c15).hash(&mut b);
|
|
0xa5u8.hash(&mut b);
|
|
Key(a, b)
|
|
}
|
|
|
|
pub fn bytes(&mut self, v: &[u8]) {
|
|
self.0.write(v);
|
|
self.1.write(v);
|
|
}
|
|
|
|
pub fn u64(&mut self, v: u64) {
|
|
self.bytes(&v.to_le_bytes());
|
|
}
|
|
|
|
pub fn f64(&mut self, v: f64) {
|
|
self.u64(v.to_bits());
|
|
}
|
|
|
|
pub fn debug(&mut self, v: &impl std::fmt::Debug) {
|
|
self.bytes(format!("{v:?}").as_bytes());
|
|
self.bytes(&[0xff]);
|
|
}
|
|
|
|
pub fn finish(&self) -> u128 {
|
|
((self.0.finish() as u128) << 64) | self.1.finish() as u128
|
|
}
|
|
}
|
|
|
|
/// Key of a GPU effect chain on `buf`: the input pixels and geometry, the bit depth, and every
|
|
/// step's effect, parameters (sorted) and context.
|
|
pub(crate) fn chain_key(chain: &[FxStep], buf: &Buf, levels: Option<f32>) -> u128 {
|
|
let mut k = Key::new(1);
|
|
k.u64(buf.img.width as u64);
|
|
k.u64(buf.img.height as u64);
|
|
k.bytes(bytemuck::cast_slice(&buf.img.data));
|
|
k.f64(buf.offset[0]);
|
|
k.f64(buf.offset[1]);
|
|
k.f64(buf.scale);
|
|
k.debug(&levels);
|
|
for s in chain {
|
|
let c = &s.ctx;
|
|
k.bytes(s.spec.id.as_bytes());
|
|
let mut names: Vec<&String> = c.params.values.keys().collect();
|
|
names.sort();
|
|
for n in names {
|
|
k.bytes(n.as_bytes());
|
|
k.debug(&c.params.values[n]);
|
|
}
|
|
k.f64(c.time);
|
|
k.f64(c.layer_size[0]);
|
|
k.f64(c.layer_size[1]);
|
|
k.u64(c.seed as u64);
|
|
k.u64(c.adjustment as u64);
|
|
let e = &c.env;
|
|
k.debug(&e.masks);
|
|
k.u64(e.host.is_some() as u64);
|
|
k.f64(e.comp_time);
|
|
k.f64(e.frame_rate);
|
|
k.u64(e.effect_index as u64);
|
|
k.debug(&e.working_space);
|
|
k.u64(e.working_linear as u64);
|
|
k.debug(&e.shutter);
|
|
}
|
|
k.finish()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
struct WakeCount(std::sync::atomic::AtomicUsize);
|
|
impl std::task::Wake for WakeCount {
|
|
fn wake(self: Arc<Self>) {
|
|
self.0.fetch_add(1, Ordering::SeqCst);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn cancellation_wakes_waiters_and_stale_same_key_does_not_drain_new_work() {
|
|
let d = Arc::new(Deferred::default());
|
|
let old = d.start(7, 1, 1, [0.0; 2], 1.0);
|
|
let wake = Arc::new(WakeCount(std::sync::atomic::AtomicUsize::new(0)));
|
|
let waker = Waker::from(wake.clone());
|
|
let mut settled = std::pin::pin!(d.settled());
|
|
for _ in 0..4 {
|
|
assert!(settled.as_mut().poll(&mut Context::from_waker(&waker)).is_pending());
|
|
}
|
|
d.cancel();
|
|
assert_eq!(wake.0.load(Ordering::SeqCst), 1);
|
|
assert_eq!(d.in_flight(), 0);
|
|
assert!(settled.as_mut().poll(&mut Context::from_waker(&waker)).is_ready());
|
|
assert!(matches!(d.lookup(7), Lookup::Ready(None)));
|
|
d.frame_begin();
|
|
let new = d.start(7, 1, 1, [0.0; 2], 1.0);
|
|
old(Some(vec![99; 4]));
|
|
assert_eq!(d.in_flight(), 1);
|
|
assert!(matches!(d.lookup(7), Lookup::InFlight));
|
|
new(None);
|
|
assert_eq!(d.in_flight(), 0);
|
|
assert!(matches!(d.lookup(7), Lookup::Ready(None)));
|
|
}
|
|
|
|
#[test]
|
|
fn device_failure_without_map_callback_drains_and_wakes_deferred() {
|
|
let d = Arc::new(Deferred::default());
|
|
let registry = crate::readback::Readbacks::new().unwrap();
|
|
let done = d.start(11, 1, 1, [0.0; 2], 1.0);
|
|
let ticket = registry.start(move |r| done(r.ok())).unwrap();
|
|
let wake = Arc::new(WakeCount(std::sync::atomic::AtomicUsize::new(0)));
|
|
let waker = Waker::from(wake.clone());
|
|
let mut settled = std::pin::pin!(d.settled());
|
|
assert!(settled.as_mut().poll(&mut Context::from_waker(&waker)).is_pending());
|
|
registry.retire("synthetic device loss".into());
|
|
assert_eq!(wake.0.load(Ordering::SeqCst), 1);
|
|
assert!(settled.as_mut().poll(&mut Context::from_waker(&waker)).is_ready());
|
|
assert_eq!(d.in_flight(), 0);
|
|
ticket.complete(Ok(vec![99; 4]));
|
|
assert!(matches!(d.lookup(11), Lookup::Ready(None)));
|
|
}
|
|
|
|
#[test]
|
|
fn keys_are_stable_and_distinct() {
|
|
let mut a = Key::new(1);
|
|
a.bytes(b"abc");
|
|
let mut b = Key::new(1);
|
|
b.bytes(b"abc");
|
|
let mut c = Key::new(2);
|
|
c.bytes(b"abc");
|
|
assert_eq!(a.finish(), b.finish());
|
|
assert_ne!(a.finish(), c.finish());
|
|
}
|
|
|
|
#[test]
|
|
fn readbacks_resolve_and_old_frames_are_dropped() {
|
|
let d = Arc::new(Deferred::default());
|
|
d.frame_begin();
|
|
d.pass_begin();
|
|
assert!(matches!(d.lookup(7), Lookup::Absent));
|
|
let done = d.start(7, 2, 1, [1.0, 2.0], 0.5);
|
|
d.miss();
|
|
assert!(d.missed() && d.gate().load(Ordering::Relaxed));
|
|
assert!(matches!(d.lookup(7), Lookup::InFlight));
|
|
assert_eq!(d.in_flight(), 1);
|
|
// The settle future wakes when the last readback lands.
|
|
let mut fut = std::pin::pin!(d.settled());
|
|
let waker = Waker::noop();
|
|
assert!(fut.as_mut().poll(&mut Context::from_waker(waker)).is_pending());
|
|
done(Some(vec![1, 2, 3, 4, 5, 6, 7, 8]));
|
|
assert!(fut.as_mut().poll(&mut Context::from_waker(waker)).is_ready());
|
|
let Lookup::Ready(Some(r)) = d.lookup(7) else { panic!("not ready") };
|
|
assert_eq!((r.width, r.height, r.offset, r.scale, r.bytes.len()), (2, 1, [1.0, 2.0], 0.5, 8));
|
|
d.pass_begin();
|
|
assert!(!d.missed());
|
|
// A readback of the previous frame arriving late is dropped.
|
|
let late = d.start(9, 1, 1, [0.0; 2], 1.0);
|
|
d.frame_begin();
|
|
late(Some(vec![0; 4]));
|
|
assert!(matches!(d.lookup(9), Lookup::Absent));
|
|
assert!(matches!(d.lookup(7), Lookup::Absent));
|
|
assert_eq!(d.in_flight(), 0);
|
|
}
|
|
}
|