mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 20:41:25 +00:00
Render: plain footage is copied, converted and quantised in one pass
A footage layer's frame was copied from the footage cache, converted from its colour profile, then quantised to the project depth in masked_source: three passes over a 33 MB 1080p float frame. Footage without masks now comes quantised from one fused pass (Pipe::copy_converted); other source() callers still get unquantised pixels. Bit-identical (tested at 8/16/32 bpc).
This commit is contained in:
1 parent
42989edd8e
commit
b3dc49a0cf
2 files changed
+110
-23
No files matched your search
@@ -153,6 +153,26 @@ impl Pipe {
|
||||
p
|
||||
}
|
||||
|
||||
/// A copy of `src` converted by `c` and quantised to the bit depth, in one pass: one read and
|
||||
/// one write of the frame instead of a copy, a conversion and a quantise pass (a 1080p float
|
||||
/// frame is 33 MB, and these passes are limited by memory speed). The same pixels as
|
||||
/// [`convert`] then [`Pipe::quantize`].
|
||||
pub fn copy_converted(&self, src: &Image, c: Option<&Conversion>) -> Image {
|
||||
let levels = self.levels;
|
||||
let data = src
|
||||
.data
|
||||
.par_iter()
|
||||
.map(|&p| {
|
||||
let mut p = c.map_or(p, |c| convert_px(p, c));
|
||||
if let Some(l) = levels {
|
||||
quantize_px(&mut p, l, 1.0 / l);
|
||||
}
|
||||
p
|
||||
})
|
||||
.collect();
|
||||
Image { width: src.width, height: src.height, data }
|
||||
}
|
||||
|
||||
/// Clamp and quantise to the bit depth (no-op in 32 bpc).
|
||||
pub fn quantize(&self, img: &mut Image) {
|
||||
self.quantize_region(img, Region::Full);
|
||||
@@ -265,14 +285,18 @@ fn quantize_rect(img: &mut Image, r: PxRect, levels: f32) {
|
||||
|
||||
/// Convert premultiplied pixels (on straight colour).
|
||||
pub fn convert(img: &mut Image, c: &Conversion) {
|
||||
img.data.par_iter_mut().for_each(|p| {
|
||||
let a = p[3];
|
||||
if a <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let o = c.apply([p[0] / a, p[1] / a, p[2] / a]);
|
||||
*p = [o[0] * a, o[1] * a, o[2] * a, a];
|
||||
});
|
||||
img.data.par_iter_mut().for_each(|p| *p = convert_px(*p, c));
|
||||
}
|
||||
|
||||
/// [`convert`] of one pixel.
|
||||
#[inline]
|
||||
fn convert_px(p: [f32; 4], c: &Conversion) -> [f32; 4] {
|
||||
let a = p[3];
|
||||
if a <= 0.0 {
|
||||
return p;
|
||||
}
|
||||
let o = c.apply([p[0] / a, p[1] / a, p[2] / a]);
|
||||
[o[0] * a, o[1] * a, o[2] * a, a]
|
||||
}
|
||||
|
||||
/// Compand: an exponential knee from 80%, approaching 1.0 (linear light, per channel).
|
||||
@@ -349,6 +373,35 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The fused footage pass gives the pixels of a copy, a conversion and a quantise pass, bit
|
||||
/// for bit, at every depth, with and without a conversion.
|
||||
#[test]
|
||||
fn copy_converted_is_convert_then_quantize() {
|
||||
let mut img = Image::new(17, 5);
|
||||
for (i, p) in img.data.iter_mut().enumerate() {
|
||||
*p = match i % 7 {
|
||||
0 => [0.3, -0.2, 0.71, 0.9],
|
||||
1 => [-0.0, 0.0, 0.0, 0.0],
|
||||
2 => [1.7, 0.4, f32::NAN, 0.5],
|
||||
3 => [0.0; 4],
|
||||
_ => [i as f32 / 90.0, 0.5, 0.25, 1.0],
|
||||
};
|
||||
}
|
||||
for depth in [effectcraft_project::BitDepth::Bpc8, effectcraft_project::BitDepth::Bpc16, effectcraft_project::BitDepth::Bpc32] {
|
||||
let pipe = Pipe::of(&ProjectSettings { bit_depth: depth, ..Default::default() });
|
||||
for c in [None, Some(Conversion::linearize(ColorSpace::Srgb))] {
|
||||
let mut want = img.clone();
|
||||
if let Some(c) = &c {
|
||||
convert(&mut want, c);
|
||||
}
|
||||
pipe.quantize(&mut want);
|
||||
let got = pipe.copy_converted(&img, c.as_ref());
|
||||
let bits = |i: &Image| i.data.iter().map(|p| p.map(f32::to_bits)).collect::<Vec<_>>();
|
||||
assert_eq!(bits(&got), bits(&want), "{depth:?} conversion {}", c.is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Both forms `round_half_up` picks between agree on everything quantising feeds it:
|
||||
/// 0..=levels, exact halves included.
|
||||
#[test]
|
||||
|
||||
@@ -878,6 +878,12 @@ impl<'a> Renderer<'a> {
|
||||
|
||||
/// Source pixels of a layer in layer space (before masks/effects).
|
||||
pub(crate) fn source(&self, ctx: &EvalCtx, layer: &Layer) -> Option<Buf> {
|
||||
self.source_with(ctx, layer, false)
|
||||
}
|
||||
|
||||
/// [`Renderer::source`]; with `quantized`, footage that [`Renderer::prequantized_footage`]
|
||||
/// counts comes quantised to the project depth.
|
||||
fn source_with(&self, ctx: &EvalCtx, layer: &Layer, quantized: bool) -> Option<Buf> {
|
||||
let s = self.opts.scale;
|
||||
match &layer.source {
|
||||
LayerSource::Solid { item } => {
|
||||
@@ -910,7 +916,7 @@ impl<'a> Renderer<'a> {
|
||||
&& let Some(nc) = self.project.comp(*item)
|
||||
{
|
||||
let img = self.footage_frame(ctx, layer, *item, pf, lt, None)?;
|
||||
return Some(self.footage_buf(&img, pf, nc.width, nc.height));
|
||||
return Some(self.footage_buf(&img, pf, nc.width, nc.height, false));
|
||||
}
|
||||
// Essential Properties overrides render the nested comp with this instance's
|
||||
// values.
|
||||
@@ -959,7 +965,7 @@ impl<'a> Renderer<'a> {
|
||||
}
|
||||
}
|
||||
let (img, pf) = self.footage_picture_of(ctx, layer, item, f)?;
|
||||
Some(self.footage_buf(&img, pf, f.width, f.height))
|
||||
Some(self.footage_buf(&img, pf, f.width, f.height, quantized && Self::prequantized_footage(layer)))
|
||||
}
|
||||
// (the font files go to this render's log, which `ctx` may not carry: see
|
||||
// [`Renderer::with_reads`])
|
||||
@@ -992,15 +998,42 @@ impl<'a> Renderer<'a> {
|
||||
|
||||
/// A decoded frame of `f` as a layer buffer covering `w`×`h` layer pixels (the footage's
|
||||
/// nominal size; proxies are scaled up to it), converted from its colour profile.
|
||||
fn footage_buf(&self, img: &Image, f: &Footage, w: u32, h: u32) -> Buf {
|
||||
let mut buf = match self.footage_layout(img, w, h) {
|
||||
FootageLayout::Native { scale } => Buf { img: img.clone(), offset: [0.0; 2], scale },
|
||||
FootageLayout::Resample { width, height, scale } => Buf { img: effectcraft_raster::resample(img, width, height), offset: [0.0; 2], scale },
|
||||
};
|
||||
if let Some(c) = self.pipe.media_in(f) {
|
||||
color::convert(&mut buf.img, &c);
|
||||
///
|
||||
/// `quantized`: also clamp and quantise it to the project depth, as [`Renderer::masked_source`]
|
||||
/// would (fused with the copy of the cached frame: one pass over it instead of three).
|
||||
fn footage_buf(&self, img: &Image, f: &Footage, w: u32, h: u32, quantized: bool) -> Buf {
|
||||
let c = self.pipe.media_in(f);
|
||||
match self.footage_layout(img, w, h) {
|
||||
FootageLayout::Native { scale } => {
|
||||
let img = match (quantized, &c) {
|
||||
(true, _) => self.pipe.copy_converted(img, c.as_ref()),
|
||||
(false, Some(c)) => {
|
||||
let mut img = img.clone();
|
||||
color::convert(&mut img, c);
|
||||
img
|
||||
}
|
||||
(false, None) => img.clone(),
|
||||
};
|
||||
Buf { img, offset: [0.0; 2], scale }
|
||||
}
|
||||
FootageLayout::Resample { width, height, scale } => {
|
||||
let mut img = effectcraft_raster::resample(img, width, height);
|
||||
if let Some(c) = &c {
|
||||
color::convert(&mut img, c);
|
||||
}
|
||||
if quantized {
|
||||
self.pipe.quantize(&mut img);
|
||||
}
|
||||
Buf { img, offset: [0.0; 2], scale }
|
||||
}
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
/// The layer's source is footage that [`Renderer::source`] hands over already quantised
|
||||
/// ([`Renderer::footage_buf`]): footage without masks (masks apply to unquantised pixels).
|
||||
/// Collapsed layers may be vector footage, drawn elsewhere: not counted.
|
||||
fn prequantized_footage(layer: &Layer) -> bool {
|
||||
matches!(layer.source, LayerSource::Footage { .. }) && !layer.switches.collapse && layer.masks().is_none_or(|m| m.groups().next().is_none())
|
||||
}
|
||||
|
||||
/// How [`Renderer::footage_buf`] lays out a decoded frame `img` of footage `w`×`h` layer
|
||||
@@ -1159,13 +1192,14 @@ 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 = match self.content_times(ctx, layer) {
|
||||
Some(times) => self.blurred_source(ctx, layer, ×)?,
|
||||
None => self.source(ctx, layer)?,
|
||||
let (mut buf, quantized) = match self.content_times(ctx, layer) {
|
||||
Some(times) => (self.blurred_source(ctx, layer, ×)?, false),
|
||||
None => (self.source_with(ctx, layer, true)?, Self::prequantized_footage(layer)),
|
||||
};
|
||||
// A solid is filled with a quantised colour: only masks can take it off the grid.
|
||||
// A solid is filled with a quantised colour, and plain footage comes quantised: only
|
||||
// masks can take them off the grid.
|
||||
let solid = matches!(layer.source, LayerSource::Solid { .. });
|
||||
if masks::apply(ctx, layer, &mut buf, self.mask_blur(ctx, layer)) || !solid {
|
||||
if masks::apply(ctx, layer, &mut buf, self.mask_blur(ctx, layer)) || !(solid || quantized) {
|
||||
self.pipe.quantize(&mut buf.img);
|
||||
}
|
||||
Some(buf)
|
||||
|
||||
Reference in new issue
Block a user