mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-13 13:52:16 +00:00
Footage: 8 bpc renders keep plain movie frames as 8-bit levels
An 8 bpc render at Full quantises plain footage straight away, so the media pool now keeps those movie frames as the 8-bit levels it is quantised to (a quarter of the memory) and the renderer expands them into the layer buffer: the same pixels to the bit. Other depths, reduced renders, masks, colour profiles, fields, frame blending, Cineon, Invert Alpha, linear light and 10/12-bit or float movies keep the float path. First playback of plain 1080p clips at Full caches ~43% faster on the CPU, and the footage cache holds 4x the frames.
This commit is contained in:
1 parent
50a889737e
commit
76e4008151
7 files changed
+334
-44
No files matched your search
@@ -37,6 +37,14 @@ pub trait FootageSource: Send + Sync {
|
||||
None => self.frame(item, footage, t),
|
||||
}
|
||||
}
|
||||
/// [`FootageSource::frame_with`]'s full-size frame as 8-bit levels: each pixel is
|
||||
/// [`effectcraft_raster::quantize_px_u8`] of the float pixel `frame_with` would give, so
|
||||
/// [`effectcraft_raster::expand_u8`] makes that frame quantised to 8 bpc, bit for bit, from a
|
||||
/// quarter of the memory (an 8 bpc render of 8-bit footage). `None` when the source has no
|
||||
/// cheaper way than the float frame; the renderer then takes that.
|
||||
fn frame8(&self, _item: ItemId, _footage: &Footage, _t: Tick, _float: FloatCurve) -> Option<Arc<crate::disk_cache::Frame8>> {
|
||||
None
|
||||
}
|
||||
/// `frames` interleaved stereo sample frames of `item`'s audio from source time `t` at
|
||||
/// `rate` Hz (`None` when unavailable). The first sample is `t.to_units_floor(rate)`;
|
||||
/// a sample boundary uses the earliest representable tick that selects that sample.
|
||||
@@ -215,6 +223,9 @@ impl FootageSource for CancellableFootage<'_> {
|
||||
fn frame_with(&self, item: ItemId, footage: &Footage, t: Tick, size: Option<(u32, u32)>, float: FloatCurve) -> Option<Arc<Image>> {
|
||||
self.read(|| self.inner.frame_with(item, footage, t, size, float))
|
||||
}
|
||||
fn frame8(&self, item: ItemId, footage: &Footage, t: Tick, float: FloatCurve) -> Option<Arc<crate::disk_cache::Frame8>> {
|
||||
self.read(|| self.inner.frame8(item, footage, t, float))
|
||||
}
|
||||
}
|
||||
|
||||
/// Footage that can be rasterised at any scale: SVG, PDF, Illustrator and EPS files, and a
|
||||
|
||||
@@ -964,6 +964,10 @@ impl<'a> Renderer<'a> {
|
||||
return Some(buf);
|
||||
}
|
||||
}
|
||||
// 8 bpc plain footage: made from the footage's 8-bit levels, no float frame.
|
||||
if quantized && let Some(buf) = self.footage_buf8(ctx, layer, item, self.proxy_for(item).unwrap_or(f), f, ctx.source_time(layer)) {
|
||||
return Some(buf);
|
||||
}
|
||||
let (img, pf) = self.footage_picture_of(ctx, layer, item, f)?;
|
||||
Some(self.footage_buf(&img, pf, f.width, f.height, quantized && Self::prequantized_footage(layer)))
|
||||
}
|
||||
@@ -1029,6 +1033,35 @@ impl<'a> Renderer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Renderer::footage_buf`]'s quantised buffer made from the footage's 8-bit levels
|
||||
/// ([`FootageSource::frame8`]), the same pixels to the bit, when nothing before the quantise
|
||||
/// changes them: an 8 bpc render at Full size of plain footage (no masks, colour profile,
|
||||
/// Separate Fields, Frame Blending, Cineon, Invert Alpha or linear light interpretation).
|
||||
/// `None` otherwise, and when the source has no 8-bit frame.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn footage_buf8(&self, ctx: &EvalCtx, layer: &Layer, item: ItemId, pf: &Footage, f: &Footage, t: Tick) -> Option<Buf> {
|
||||
let plain = self.pipe.levels == Some(255.0)
|
||||
&& Self::prequantized_footage(layer)
|
||||
&& self.reduced_size(f.width, f.height).is_none()
|
||||
&& self.pipe.media_in(pf).is_none()
|
||||
&& pf.fields == effectcraft_project::FieldOrder::Off
|
||||
&& !(pf.invert_alpha || pf.linear_light_applies() || pf.cineon_applies())
|
||||
&& frame_blend_mode(ctx, layer) == FrameBlend::Off;
|
||||
if !plain {
|
||||
return None;
|
||||
}
|
||||
let fr = self.footage.frame8(item, pf, t, self.pipe.float_curve(pf))?;
|
||||
let n = (fr.width as usize).checked_mul(fr.height as usize)?;
|
||||
let px = fr.rgba.as_chunks::<4>().0;
|
||||
if px.len() != n {
|
||||
return None;
|
||||
}
|
||||
use rayon::prelude::*;
|
||||
let data = px.par_iter().map(|&p| effectcraft_raster::expand_u8(p)).collect();
|
||||
let k = if fr.width > 0 { f.width.max(1) as f64 / fr.width as f64 } else { 1.0 };
|
||||
Some(Buf { img: Image { width: fr.width, height: fr.height, data }, offset: [0.0; 2], scale: 1.0 / k })
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
@@ -106,3 +106,54 @@ fn still_layers_are_cached_across_times_and_movies_are_not() {
|
||||
assert_eq!(src.sizes.unwrap().into_inner().unwrap().len(), fetches + 3, "{kind:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Clip`] that also keeps its frames as 8-bit levels, counting the times it's asked for them.
|
||||
struct Clip8 {
|
||||
asked: std::sync::atomic::AtomicUsize,
|
||||
}
|
||||
|
||||
impl FootageSource for Clip8 {
|
||||
fn frame(&self, item: ItemId, f: &Footage, t: Tick) -> Option<Arc<Image>> {
|
||||
let mut img = (*Clip { sizes: None }.frame(item, f, t)?).clone();
|
||||
// (partly transparent, premultiplied, as decoded with alpha)
|
||||
img.data.iter_mut().enumerate().for_each(|(i, p)| {
|
||||
let a = (i % 7) as f32 / 6.0;
|
||||
*p = [p[0] * a, p[1] * a, p[2] * a, a];
|
||||
});
|
||||
Some(Arc::new(img))
|
||||
}
|
||||
fn frame8(&self, item: ItemId, f: &Footage, t: Tick, _: crate::FloatCurve) -> Option<Arc<crate::disk_cache::Frame8>> {
|
||||
self.asked.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let img = self.frame(item, f, t)?;
|
||||
let rgba = img.data.iter().flat_map(|p| effectcraft_raster::quantize_px_u8(*p)).collect();
|
||||
Some(Arc::new(crate::disk_cache::Frame8 { width: img.width, height: img.height, rgba }))
|
||||
}
|
||||
}
|
||||
|
||||
/// An 8 bpc render at Full takes plain footage as 8-bit levels and renders the same pixels to
|
||||
/// the bit; other depths, reduced renders and interpreted pixels never ask for them.
|
||||
#[test]
|
||||
fn footage_kept_as_8_bit_levels_renders_the_same_pixels() {
|
||||
struct Float(Clip8);
|
||||
impl FootageSource for Float {
|
||||
fn frame(&self, item: ItemId, f: &Footage, t: Tick) -> Option<Arc<Image>> {
|
||||
self.0.frame(item, f, t)
|
||||
}
|
||||
}
|
||||
for (depth, invert, scale, used) in [
|
||||
(BitDepth::Bpc8, false, 1.0, true),
|
||||
(BitDepth::Bpc8, false, 0.75, true),
|
||||
(BitDepth::Bpc8, false, 0.5, false),
|
||||
(BitDepth::Bpc8, true, 1.0, false),
|
||||
(BitDepth::Bpc16, false, 1.0, false),
|
||||
(BitDepth::Bpc32, false, 1.0, false),
|
||||
] {
|
||||
let (p, cid) = project(depth, invert);
|
||||
let opts = RenderOpts { scale, ..Default::default() };
|
||||
let q8 = Clip8 { asked: Default::default() };
|
||||
let frame = Renderer::new(&p, &q8, opts).comp_frame(cid, Tick::ZERO);
|
||||
let plain = Renderer::new(&p, &Float(Clip8 { asked: Default::default() }), opts).comp_frame(cid, Tick::ZERO);
|
||||
assert!(frame.data.iter().zip(&plain.data).all(|(a, b)| a.map(f32::to_bits) == b.map(f32::to_bits)), "{depth:?} {invert} {scale}");
|
||||
assert_eq!(q8.asked.into_inner() > 0, used, "{depth:?} {invert} {scale}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user