M4: LUT-based linear conversion and linear-light box decimation for reduced-resolution playback

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-09-30 06:45:35 -04:00
1 parent 8194dac6a8
commit 5881842a2e
2 files changed
+125 -44

No files matched your search

+109 -41
View File
@@ -100,71 +100,139 @@ impl VideoFrame {
/// Convert to premultiplied linear RGBA f32 (the compositor's working format).
pub fn to_linear_f32(&self) -> Vec<f32> {
self.to_linear_f32_decimated(1).2
}
/// Convert to premultiplied linear RGBA f32, box-filtering `n`×`n` blocks (n = 1, 2, 4, 8…)
/// in linear light. Reduced-resolution playback uses this so it never builds full-size float
/// buffers. Returns (width, height, pixels).
pub fn to_linear_f32_decimated(&self, n: usize) -> (usize, usize, Vec<f32>) {
let n = n.max(1);
let (w, h) = (self.width as usize, self.height as usize);
let mut out = vec![0f32; w * h * 4];
let (ow, oh) = ((w / n).max(1), (h / n).max(1));
let mut out = vec![0f32; ow * oh * 4];
if let PixelData::RgbaF32(d) = &self.data
&& n == 1
{
out.copy_from_slice(d);
return (ow, oh, out);
}
// Encoded (0..1, quantised to 12 bits) → linear lookup for this frame's transfer.
let info = self.color;
let lin: Vec<f32> = (0..4096).map(|i| to_linear(i as f32 / 4095.0, info.transfer)).collect();
let q = |v: f32| lin[(v.clamp(0.0, 1.0) * 4095.0 + 0.5) as usize];
let inv = 1.0 / (n * n) as f32;
match &self.data {
PixelData::RgbaF32(d) => out.copy_from_slice(d),
PixelData::RgbaF32(d) => {
out.par_chunks_mut(ow * 4).enumerate().for_each(|(oy, row)| {
for ox in 0..ow {
let mut acc = [0f32; 4];
for dy in 0..n {
let y = (oy * n + dy).min(h - 1);
for dx in 0..n {
let x = (ox * n + dx).min(w - 1);
let i = (y * w + x) * 4;
for k in 0..4 {
acc[k] += d[i + k];
}
}
}
for k in 0..4 {
row[ox * 4 + k] = acc[k] * inv;
}
}
});
}
PixelData::Rgba8(d) => {
let lut = srgb_u8_to_linear_table();
out.par_chunks_mut(w * 4).zip(d.par_chunks(w * 4)).for_each(|(o, s)| {
for (o, s) in o.chunks_exact_mut(4).zip(s.chunks_exact(4)) {
let a = s[3] as f32 / 255.0;
o[0] = lut[s[0] as usize] * a;
o[1] = lut[s[1] as usize] * a;
o[2] = lut[s[2] as usize] * a;
o[3] = a;
out.par_chunks_mut(ow * 4).enumerate().for_each(|(oy, row)| {
for ox in 0..ow {
let mut acc = [0f32; 4];
for dy in 0..n {
let y = (oy * n + dy).min(h - 1);
for dx in 0..n {
let x = (ox * n + dx).min(w - 1);
let s = &d[(y * w + x) * 4..(y * w + x) * 4 + 4];
let a = s[3] as f32 / 255.0;
acc[0] += lut[s[0] as usize] * a;
acc[1] += lut[s[1] as usize] * a;
acc[2] += lut[s[2] as usize] * a;
acc[3] += a;
}
}
for k in 0..4 {
row[ox * 4 + k] = acc[k] * inv;
}
}
});
}
PixelData::Yuv8 { planes, chroma, alpha } => {
let (sx, sy) = chroma.shifts();
let cw = w.div_ceil(1 << sx);
let info = self.color;
// Precompute per-code tables for Y and C.
let ytab: Vec<f32> = (0..256).map(|v| normalize_y(v, 8, info.range)).collect();
let ctab: Vec<f32> = (0..256).map(|v| normalize_c(v, 8, info.range)).collect();
out.par_chunks_mut(w * 4).enumerate().for_each(|(y, row)| {
let yrow = &planes[0][y * w..y * w + w];
let cy = y >> sy;
let urow = &planes[1][cy * cw..cy * cw + cw];
let vrow = &planes[2][cy * cw..cy * cw + cw];
for x in 0..w {
let cx = x >> sx;
let rgb = ycbcr_to_rgb(ytab[yrow[x] as usize], ctab[urow[cx] as usize], ctab[vrow[cx] as usize], info.matrix);
let a = alpha.as_ref().map_or(1.0, |al| al[y * w + x] as f32 / 255.0);
let o = &mut row[x * 4..x * 4 + 4];
o[0] = to_linear(rgb[0].clamp(0.0, 1.0), info.transfer) * a;
o[1] = to_linear(rgb[1].clamp(0.0, 1.0), info.transfer) * a;
o[2] = to_linear(rgb[2].clamp(0.0, 1.0), info.transfer) * a;
o[3] = a;
let (kr, kb) = info.matrix.kr_kb();
let kg = 1.0 - kr - kb;
let (cr_r, cb_b) = (2.0 * (1.0 - kr), 2.0 * (1.0 - kb));
let (cr_g, cb_g) = (cr_r * kr / kg, cb_b * kb / kg);
out.par_chunks_mut(ow * 4).enumerate().for_each(|(oy, row)| {
for ox in 0..ow {
let mut acc = [0f32; 4];
for dy in 0..n {
let y = (oy * n + dy).min(h - 1);
let cy = y >> sy;
for dx in 0..n {
let x = (ox * n + dx).min(w - 1);
let cx = x >> sx;
let yy = ytab[planes[0][y * w + x] as usize];
let u = ctab[planes[1][cy * cw + cx] as usize];
let v = ctab[planes[2][cy * cw + cx] as usize];
let a = alpha.as_ref().map_or(1.0, |al| al[y * w + x] as f32 / 255.0);
acc[0] += q(yy + cr_r * v) * a;
acc[1] += q(yy - cr_g * v - cb_g * u) * a;
acc[2] += q(yy + cb_b * u) * a;
acc[3] += a;
}
}
for k in 0..4 {
row[ox * 4 + k] = acc[k] * inv;
}
}
});
}
PixelData::Yuv16 { planes, chroma, bits, alpha } => {
let (sx, sy) = chroma.shifts();
let cw = w.div_ceil(1 << sx);
let info = self.color;
let bits = *bits;
let amax = ((1u32 << bits) - 1) as f32;
out.par_chunks_mut(w * 4).enumerate().for_each(|(y, row)| {
let cy = y >> sy;
for x in 0..w {
let cx = x >> sx;
let yy = normalize_y(planes[0][y * w + x] as u32, bits, info.range);
let u = normalize_c(planes[1][cy * cw + cx] as u32, bits, info.range);
let v = normalize_c(planes[2][cy * cw + cx] as u32, bits, info.range);
let rgb = ycbcr_to_rgb(yy, u, v, info.matrix);
let a = alpha.as_ref().map_or(1.0, |al| al[y * w + x] as f32 / amax);
let o = &mut row[x * 4..x * 4 + 4];
o[0] = to_linear(rgb[0].clamp(0.0, 1.0), info.transfer) * a;
o[1] = to_linear(rgb[1].clamp(0.0, 1.0), info.transfer) * a;
o[2] = to_linear(rgb[2].clamp(0.0, 1.0), info.transfer) * a;
o[3] = a;
out.par_chunks_mut(ow * 4).enumerate().for_each(|(oy, row)| {
for ox in 0..ow {
let mut acc = [0f32; 4];
for dy in 0..n {
let y = (oy * n + dy).min(h - 1);
let cy = y >> sy;
for dx in 0..n {
let x = (ox * n + dx).min(w - 1);
let cx = x >> sx;
let yy = normalize_y(planes[0][y * w + x] as u32, bits, info.range);
let u = normalize_c(planes[1][cy * cw + cx] as u32, bits, info.range);
let v = normalize_c(planes[2][cy * cw + cx] as u32, bits, info.range);
let rgb = ycbcr_to_rgb(yy, u, v, info.matrix);
let a = alpha.as_ref().map_or(1.0, |al| al[y * w + x] as f32 / amax);
acc[0] += q(rgb[0]) * a;
acc[1] += q(rgb[1]) * a;
acc[2] += q(rgb[2]) * a;
acc[3] += a;
}
}
for k in 0..4 {
row[ox * 4 + k] = acc[k] * inv;
}
}
});
}
}
out
(ow, oh, out)
}
/// Convert to straight-alpha sRGB RGBA8 for display (fast paths for 8-bit sources).
+16 -3
View File
@@ -207,8 +207,9 @@ fn item_layer(
ItemKind::Media(_) | ItemKind::Subclip { .. } => {
let src = sources.source(item.item)?;
let frame = src.video_frame(FrameRequest { time: mt, scale: want }).ok()?;
let data = frame.to_linear_f32();
Image { w: frame.width as usize, h: frame.height as usize, px: data }
let n = decimation(frame.width as f32, src_size.0 as f32 * want);
let (w, h, px) = frame.to_linear_f32_decimated(n);
Image { w, h, px }
}
ItemKind::Sequence(nested) => {
let sub = RenderOptions { scale: want, effects: opts.effects, depth: opts.depth + 1 };
@@ -242,6 +243,15 @@ fn item_layer(
Some((placed, op, bl))
}
/// Largest power-of-two box decimation that keeps at least `target_w` pixels of width.
fn decimation(have_w: f32, target_w: f32) -> usize {
let mut n = 1usize;
while n < 16 && have_w / (n as f32 * 2.0) >= target_w.max(1.0) {
n *= 2;
}
n
}
/// Render a single project item (e.g. for the Source monitor) at media time `t`.
pub fn render_item(project: &Project, item: ItemId, t: Tick, scale: f32, sources: &dyn SourceProvider) -> Option<Image> {
let pi = project.item(item)?;
@@ -250,7 +260,10 @@ pub fn render_item(project: &Project, item: ItemId, t: Tick, scale: f32, sources
_ => {
let src = sources.source(item)?;
let f = src.video_frame(FrameRequest { time: t, scale }).ok()?;
Some(Image { w: f.width as usize, h: f.height as usize, px: f.to_linear_f32() })
let full_w = src.info().video.as_ref().map_or(f.width, |v| v.width) as f32;
let n = decimation(f.width as f32, full_w * scale);
let (w, h, px) = f.to_linear_f32_decimated(n);
Some(Image { w, h, px })
}
}
}