M12.4: fast bit-depth quantization

8/16 bpc no longer re-quantises the whole comp after every layer: 2D layers
report the canvas region they can change (transformed buffer bounds plus the
resampling margin; stencil modes, motion blur, perspective, styles,
adjustment and 3D runs fall back to the full canvas), and only that region
is quantised. A quantised buffer placed pixel-exactly (integer translation,
Normal, 100%) over a blank canvas needs nothing; invisible layers need
nothing; collapsed precomps quantise per nested layer. The redundant final
comp pass is skipped unless the comp is re-encoded. Solids are filled with a
pre-quantised colour and skip the source pass unless masked. Row-parallel
vectorisable kernel. Debug builds verify every pixel outside the region is
already quantised (whole workspace test suite run with 8 bpc forced: no
violations).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-02 11:11:10 -04:00
co-authored by Claude Opus 5.5
parent ab3b4cfb70
commit 40321f963b
4 changed files with 196 additions and 42 deletions
+97 -7
View File
@@ -73,10 +73,47 @@ impl Pipe {
Conversion::new(ws, self.linear, ColorSpace::Srgb, false)
}
/// One pixel clamped and quantised to the bit depth.
pub fn quantize_px(&self, mut p: [f32; 4]) -> [f32; 4] {
if let Some(l) = self.levels {
quantize_px(&mut p, l, 1.0 / l);
}
p
}
/// Clamp and quantise to the bit depth (no-op in 32 bpc).
pub fn quantize(&self, img: &mut Image) {
if let Some(l) = self.levels {
quantize(img, l);
self.quantize_region(img, Region::Full);
}
/// Clamp and quantise the pixels of `region` (no-op in 32 bpc). Pixels outside it must
/// already be quantised (checked in debug builds).
pub fn quantize_region(&self, img: &mut Image, region: Region) {
let Some(l) = self.levels else { return };
let r = match region {
Region::Empty => None,
Region::Full => Some(PxRect { x0: 0, y0: 0, x1: img.width, y1: img.height }),
Region::Rect(r) => Some(r.clamp_to(img.width, img.height)),
};
if let Some(r) = r {
quantize_rect(img, r, l);
}
#[cfg(debug_assertions)]
if img.data.len() <= 1 << 20 {
let inv = 1.0 / l;
let bad = img.data.iter().position(|p| {
let q = |v: f32, hi: f32| (v.clamp(0.0, hi) * l).round() * inv;
let a = q(p[3], 1.0);
(p[3] - a).abs() > 1e-5 || (0..3).any(|c| (p[c] - q(p[c], a)).abs() > 1e-5)
});
if let Some(i) = bad {
panic!(
"pixel ({}, {}) outside the quantised region {region:?} is not quantised: {:?}",
i as u32 % img.width,
i as u32 / img.width,
img.data[i]
);
}
}
}
@@ -89,14 +126,67 @@ impl Pipe {
}
}
/// A pixel rectangle `[x0, x1) × [y0, y1)`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PxRect {
pub x0: u32,
pub y0: u32,
pub x1: u32,
pub y1: u32,
}
impl PxRect {
/// The pixels covering the real-valued box `[x0, x1] × [y0, y1]` grown by `margin`.
pub fn covering(x0: f64, y0: f64, x1: f64, y1: f64, margin: f64) -> Option<PxRect> {
if !(x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite()) {
return None;
}
let c = |v: f64| v.clamp(0.0, u32::MAX as f64) as u32;
Some(PxRect { x0: c((x0 - margin).floor()), y0: c((y0 - margin).floor()), x1: c((x1 + margin).ceil()), y1: c((y1 + margin).ceil()) })
}
fn clamp_to(self, w: u32, h: u32) -> PxRect {
let (x1, y1) = (self.x1.min(w), self.y1.min(h));
PxRect { x0: self.x0.min(x1), y0: self.y0.min(y1), x1, y1 }
}
}
/// The part of a canvas that a drawing step may have changed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Region {
Empty,
Rect(PxRect),
Full,
}
/// Clamp premultiplied pixels to 0..1 (colour ≤ alpha) and round to `levels` steps.
pub fn quantize(img: &mut Image, levels: f32) {
quantize_rect(img, PxRect { x0: 0, y0: 0, x1: img.width, y1: img.height }, levels);
}
/// Clamp (colour ≤ alpha ≤ 1, NaN → 0) and round half up; values are non-negative after the
/// clamp, so the integer conversion rounds like `round()` and vectorises.
#[allow(clippy::manual_clamp)] // max/min map NaN to 0; clamp would keep NaN
#[inline(always)]
fn quantize_px(p: &mut [f32; 4], levels: f32, inv: f32) {
let a = (p[3].max(0.0).min(1.0) * levels + 0.5) as u32 as f32 * inv;
let hi = [a, a, a, a];
for c in 0..3 {
p[c] = (p[c].max(0.0).min(hi[c]) * levels + 0.5) as u32 as f32 * inv;
}
p[3] = a;
}
/// [`quantize`] restricted to a rectangle (already clamped to the image), row-parallel.
fn quantize_rect(img: &mut Image, r: PxRect, levels: f32) {
if r.x0 >= r.x1 || r.y0 >= r.y1 {
return;
}
let inv = 1.0 / levels;
img.data.par_iter_mut().for_each(|p| {
let a = (p[3].clamp(0.0, 1.0) * levels).round() * inv;
p[3] = a;
for c in 0..3 {
p[c] = (p[c].clamp(0.0, a) * levels).round() * inv;
let w = img.width as usize;
let (x0, x1) = (r.x0 as usize, r.x1 as usize);
img.data[r.y0 as usize * w..r.y1 as usize * w].par_chunks_mut(w).for_each(|row| {
for p in &mut row[x0..x1] {
quantize_px(p, levels, inv);
}
});
}
+92 -30
View File
@@ -9,6 +9,7 @@
pub mod audio;
pub mod cache;
pub mod color;
use color::Region;
pub mod eval;
pub mod masks;
pub mod shapes;
@@ -265,22 +266,25 @@ impl<'a> Renderer<'a> {
/// Draw a collapsed 2D precomp layer: nested layers blend straight into `canvas` with their
/// own modes (Normal precomp, no matte); otherwise they are drawn into an isolated buffer
/// that then takes the precomp layer's track matte, Preserve Transparency and blend mode.
fn draw_collapsed(&self, ctx: &EvalCtx<'a>, layer: &Layer, item: ItemId, canvas: &mut Image) {
/// Returns the region left to re-quantise (nested layers drawn straight into `canvas`
/// quantise their own footprints).
fn draw_collapsed(&self, ctx: &EvalCtx<'a>, layer: &Layer, item: ItemId, canvas: &mut Image, blank: bool) -> Region {
let opacity = ctx.opacity(layer) as f32 * self.opacity_mul;
if opacity <= 0.0 {
return;
return Region::Empty;
}
let matte = layer.track_matte.is_some_and(|tm| tm.layer != layer.id && ctx.comp.layer(tm.layer).is_some());
if layer.blend_mode == BlendMode::Normal && !matte && !layer.preserve_transparency {
if let Some((sub, nctx)) = self.collapse_into(ctx, layer, item, opacity) {
sub.draw_comp(&nctx, canvas);
sub.draw_comp(&nctx, canvas, blank);
}
return;
return Region::Empty;
}
let Some((sub, nctx)) = self.collapse_into(ctx, layer, item, 1.0) else { return };
let Some((sub, nctx)) = self.collapse_into(ctx, layer, item, 1.0) else { return Region::Empty };
let mut iso = Image::new(canvas.width, canvas.height);
sub.draw_comp(&nctx, &mut iso);
sub.draw_comp(&nctx, &mut iso, true);
self.composite_iso(ctx, layer, iso, canvas, opacity);
Region::Full
}
/// Rasterisation scale of a layer's source: the output scale, or — for Continuously
@@ -347,11 +351,12 @@ impl<'a> Renderer<'a> {
return canvas;
}
let ctx = self.ctx(comp_id, comp, t);
self.draw_comp(&ctx, &mut canvas);
self.draw_comp(&ctx, &mut canvas, true);
// The canvas is already quantised after every layer; only re-encoding changes it.
if let Some(c) = self.pipe.from_blend() {
color::convert(&mut canvas, &c);
self.pipe.quantize(&mut canvas);
}
self.pipe.quantize(&mut canvas);
// The top-level comp leaves the working space for the (sRGB) display / output.
if self.depth == 0
&& let Some(c) = self.pipe.output()
@@ -363,7 +368,8 @@ impl<'a> Renderer<'a> {
}
/// Draw a comp's layers (bottom to top) into `canvas` (blending space).
fn draw_comp(&self, ctx: &EvalCtx<'a>, canvas: &mut Image) {
/// `blank`: the canvas is fully transparent (lets the first layer skip re-quantising).
fn draw_comp(&self, ctx: &EvalCtx<'a>, canvas: &mut Image, mut blank: bool) {
let (comp, t) = (ctx.comp, ctx.time);
let any_solo = comp.layers.iter().any(|l| l.switches.solo && l.source.is_av() && l.is_active_at(t));
// Bottom-to-top, with runs of consecutive 3D layers depth-sorted (farthest first).
@@ -382,15 +388,19 @@ impl<'a> Renderer<'a> {
}
three_d::compose::draw_run(self, ctx, &visible[i..j], canvas);
i = j;
self.pipe.quantize(canvas);
blank = false;
} else {
match self.collapsed(ctx, visible[i]) {
Some(item) => self.draw_collapsed(ctx, visible[i], item, canvas),
None => self.draw_layer(ctx, visible[i], canvas),
}
let changed = match self.collapsed(ctx, visible[i]) {
Some(item) => self.draw_collapsed(ctx, visible[i], item, canvas, blank),
None => self.draw_layer(ctx, visible[i], canvas, blank),
};
i += 1;
// 8/16 bpc: the comp is an integer buffer after every layer (only the pixels the
// layer touched need re-quantising).
self.pipe.quantize_region(canvas, changed);
blank = false;
}
// 8/16 bpc: the comp is an integer buffer after every layer.
self.pipe.quantize(canvas);
}
}
@@ -467,7 +477,8 @@ impl<'a> Renderer<'a> {
if let Some(conv) = self.pipe.authored_in() {
c = conv.apply(c);
}
Some(Buf { img: Image::filled(w, h, [c[0], c[1], c[2], 1.0]), offset: [0.0; 2], scale: s })
let px = self.pipe.quantize_px([c[0], c[1], c[2], 1.0]);
Some(Buf { img: Image::filled(w, h, px), offset: [0.0; 2], scale: s })
}
LayerSource::Comp { item } => {
if self.project.comp_contains(*item, ctx.comp_id) {
@@ -562,8 +573,11 @@ impl<'a> Renderer<'a> {
/// Source → masks, clamped/quantised to the project depth.
fn masked_source(&self, ctx: &EvalCtx, layer: &Layer) -> Option<Buf> {
let mut buf = self.source(ctx, layer)?;
masks::apply(ctx, layer, &mut buf);
self.pipe.quantize(&mut buf.img);
// A solid is filled with a quantised colour: only masks can take it off the grid.
let solid = matches!(layer.source, LayerSource::Solid { .. });
if masks::apply(ctx, layer, &mut buf) || !solid {
self.pipe.quantize(&mut buf.img);
}
Some(buf)
}
@@ -733,13 +747,57 @@ impl<'a> Renderer<'a> {
Mat3::scale(vec2(s, s)) * l2c * Mat3::scale(vec2(1.0 / buf.scale, 1.0 / buf.scale)) * Mat3::translate(vec2(-buf.offset[0], -buf.offset[1]))
}
/// Draw a processed layer into `target` (transform, motion blur) with `mode`/`opacity`.
fn place(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf, target: &mut Image, mode: BlendMode, opacity: f32) {
let samples = if self.opts.motion_blur && ctx.comp.enable_motion_blur && layer.switches.motion_blur {
/// Motion-blur sub-samples for a layer (1 = no motion blur).
fn mb_samples(&self, ctx: &EvalCtx, layer: &Layer) -> usize {
if self.opts.motion_blur && ctx.comp.enable_motion_blur && layer.switches.motion_blur {
if self.opts.draft { 4 } else { ctx.comp.motion_blur_samples.clamp(2, 64) as usize }
} else {
1
};
}
}
/// Canvas pixels that compositing `buf` for `layer` can change: the transformed buffer's
/// bounds plus the resampling footprint. Stencil/silhouette modes, motion blur and
/// perspective fall back to the whole canvas.
///
/// A quantised buffer placed pixel-exactly (integer translation, Normal, 100%) over a blank
/// canvas leaves exactly its own (quantised) pixels: nothing to re-quantise.
fn footprint(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf, opacity: f32, blank: bool) -> Region {
if layer.blend_mode.is_stencil() || self.mb_samples(ctx, layer) > 1 {
return Region::Full;
}
let m = self.buf_matrix(ctx, layer, buf);
if !m.is_affine() {
return Region::Full;
}
let matte = layer.track_matte.is_some_and(|tm| tm.layer != layer.id && ctx.comp.layer(tm.layer).is_some());
let k = &m.0;
let integral = |v: f64| v == v.round();
if blank
&& layer.blend_mode == BlendMode::Normal
&& opacity == 1.0
&& !matte
&& !layer.preserve_transparency
&& self.pipe.to_blend().is_none()
&& k[0][0] == 1.0
&& k[1][1] == 1.0
&& k[0][1] == 0.0
&& k[1][0] == 0.0
&& integral(k[0][2])
&& integral(k[1][2])
{
return Region::Empty;
}
let (w, h) = (buf.img.width as f64, buf.img.height as f64);
let c = [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)].map(|(x, y)| m.apply(vec2(x, y)));
let (x0, x1) = (c.iter().map(|p| p.x).fold(f64::INFINITY, f64::min), c.iter().map(|p| p.x).fold(f64::NEG_INFINITY, f64::max));
let (y0, y1) = (c.iter().map(|p| p.y).fold(f64::INFINITY, f64::min), c.iter().map(|p| p.y).fold(f64::NEG_INFINITY, f64::max));
color::PxRect::covering(x0, y0, x1, y1, 3.0).map_or(Region::Full, Region::Rect)
}
/// Draw a processed layer into `target` (transform, motion blur) with `mode`/`opacity`.
fn place(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf, target: &mut Image, mode: BlendMode, opacity: f32) {
let samples = self.mb_samples(ctx, layer);
let opts = WarpOpts { sampling: self.sampling(layer), opacity, mode, seed: layer.id.0 as u32, clip: None };
if samples <= 1 {
let m = self.buf_matrix(ctx, layer, buf);
@@ -761,29 +819,32 @@ impl<'a> Renderer<'a> {
target.blend_from(&acc, mode, opacity, layer.id.0 as u32);
}
fn draw_layer(&self, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image) {
/// Draw a 2D layer into `canvas`; returns the region it may have changed.
fn draw_layer(&self, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image, blank: bool) -> Region {
let opacity = ctx.opacity(layer) as f32 * self.opacity_mul;
if layer.switches.quality == Quality::Wireframe && !layer.switches.adjustment {
self.draw_wireframe(ctx, layer, canvas);
return;
return Region::Full;
}
if opacity <= 0.0 && !layer.switches.adjustment {
return;
return Region::Empty;
}
if layer.switches.adjustment {
self.draw_adjustment(ctx, layer, canvas, opacity);
return;
return Region::Full;
}
let mut timing = self.profile.map(|_| LayerTiming { depth: self.depth, layer: layer.name.clone(), ..Default::default() });
let t0 = web_time::Instant::now();
let Some(st) = self.styled(ctx, layer, timing.as_mut()) else { return };
let Some(st) = self.styled(ctx, layer, timing.as_mut()) else { return Region::Empty };
let st = self.styled_to_blend(st);
let t1 = web_time::Instant::now();
if st.plain {
let changed = if st.plain {
self.composite_layer(ctx, layer, &st.body, canvas, opacity);
self.footprint(ctx, layer, &st.body, opacity, blank)
} else {
self.composite_styled(ctx, layer, &st, canvas, opacity);
}
Region::Full
};
if let (Some(prof), Some(mut timing)) = (self.profile, timing) {
timing.process_ms = (t1 - t0).as_secs_f64() * 1e3;
timing.composite_ms = t1.elapsed().as_secs_f64() * 1e3;
@@ -791,6 +852,7 @@ impl<'a> Renderer<'a> {
v.push(timing);
}
}
changed
}
/// Composite a styled layer: exterior passes with their own modes, then the body with the
@@ -882,7 +944,7 @@ impl<'a> Renderer<'a> {
/// Adjustment layer: effects applied to the comp below, limited to the layer's footprint.
fn draw_adjustment(&self, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image, opacity: f32) {
let Some(mut foot) = self.source(ctx, layer) else { return };
masks::apply(ctx, layer, &mut foot);
let _ = masks::apply(ctx, layer, &mut foot);
let mut matte = Image::new(canvas.width, canvas.height);
self.place(ctx, layer, &foot, &mut matte, BlendMode::Normal, 1.0);
let mut below = Buf { img: canvas.clone(), offset: [0.0; 2], scale: self.opts.scale };
+5 -3
View File
@@ -113,11 +113,12 @@ pub fn shapes(ctx: &EvalCtx, layer: &Layer) -> Vec<effectcraft_effects::MaskShap
}
/// Apply the layer's masks to its buffer.
pub fn apply(ctx: &EvalCtx, layer: &Layer, buf: &mut Buf) {
let Some(masks) = layer.masks() else { return };
/// Returns whether any mask was applied.
pub fn apply(ctx: &EvalCtx, layer: &Layer, buf: &mut Buf) -> bool {
let Some(masks) = layer.masks() else { return false };
let list: Vec<_> = masks.groups().filter(|g| g.enabled && matches!(g.kind, GroupKind::Mask { mode, .. } if mode != MaskMode::None)).collect();
if list.is_empty() {
return;
return false;
}
// Masks may extend past the source: grow the buffer to cover them when feather/expansion pads.
let n = (buf.img.width * buf.img.height) as usize;
@@ -146,4 +147,5 @@ pub fn apply(ctx: &EvalCtx, layer: &Layer, buf: &mut Buf) {
});
}
buf.img.mul_mask(&acc);
true
}
+2 -2
View File
@@ -316,7 +316,7 @@ pub(crate) fn draw_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], canvas: &mut
// 3D adjustment layers act on everything below them in the stack (like 2D ones): split.
if let Some(k) = run.iter().position(|l| l.switches.adjustment) {
draw_run(r, ctx, &run[..k], canvas);
r.draw_layer(ctx, run[k], canvas);
r.draw_layer(ctx, run[k], canvas, false);
draw_run(r, ctx, &run[k + 1..], canvas);
return;
}
@@ -363,7 +363,7 @@ pub(crate) fn draw_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], canvas: &mut
let casters: Vec<&Item> = items.iter().chain(extra.iter()).filter(|i| i.mat.casts_shadows != 0 && !i.geos.is_empty()).collect();
composite(canvas, &items, &lights, &casters, run[0].id.0 as u32);
for l in run.iter().filter(|l| l.switches.quality == effectcraft_project::Quality::Wireframe) {
r.draw_layer(ctx, l, canvas);
r.draw_layer(ctx, l, canvas, false);
}
}