593 lines
25 KiB
Rust
593 lines
25 KiB
Rust
//! The wgpu compositor.
|
|
//!
|
|
//! Executes a [`FramePlan`](filmcraft_render::plan::FramePlan) on the GPU: every layer's source is
|
|
//! uploaded as textures (Y/Cb/Cr planes stay YUV — conversion to linear RGB happens per pixel in
|
|
//! the shader), drawn as a transformed quad with premultiplied "over" blending into a Rgba16Float
|
|
//! accumulator, then resolved over black into an sRGB `Rgba8UnormSrgb` texture that the UI
|
|
//! registers as a native texture. Uploads are cached by pixel-buffer identity, so a paused frame or
|
|
//! a still costs nothing.
|
|
//!
|
|
//! Layers that need converting before upload (linear f32 RGBA from CPU-rendered layers, 16-bit
|
|
//! YUV such as ProRes) are converted to half floats by [`prepare`], which frame workers run off the
|
|
//! UI thread; [`GpuCompositor::composite_prepared`] then only copies bytes into textures.
|
|
//!
|
|
//! The CPU plan executor (`filmcraft_render::plan::execute_cpu`) is the oracle; tests compare.
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
|
|
use rayon::prelude::*;
|
|
|
|
use filmcraft_color::{Matrix, Range, Transfer};
|
|
use filmcraft_frame::{Chroma, PixelData, VideoFrame};
|
|
use filmcraft_render::plan::{FramePlan, PlanLayer};
|
|
|
|
pub mod lut;
|
|
pub mod mask;
|
|
pub use lut::GpuLut;
|
|
pub use mask::GpuMask;
|
|
|
|
/// Output texture format: gamma-encoded RGBA8 (what egui expects of native textures); the resolve
|
|
/// shader applies the sRGB encoding.
|
|
pub const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
|
const ACCUM_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
|
|
|
struct Uploaded {
|
|
views: [wgpu::TextureView; 3],
|
|
kind: u32,
|
|
code_scale: f32,
|
|
chroma: (u32, u32),
|
|
last_used: u64,
|
|
/// The uploaded pixel buffers, kept alive while cached: the cache key is the buffer address,
|
|
/// and a freed buffer's address can be reused by a different frame (stale texture).
|
|
_pixels: PixelData,
|
|
}
|
|
|
|
pub struct GpuCompositor {
|
|
device: wgpu::Device,
|
|
queue: wgpu::Queue,
|
|
layer_pipeline: wgpu::RenderPipeline,
|
|
final_pipeline: wgpu::RenderPipeline,
|
|
layer_bgl: wgpu::BindGroupLayout,
|
|
final_bgl: wgpu::BindGroupLayout,
|
|
accum: Option<(wgpu::Texture, wgpu::TextureView, (u32, u32))>,
|
|
output: Option<(wgpu::Texture, wgpu::TextureView, (u32, u32))>,
|
|
uploads: HashMap<(usize, u32, u32), Uploaded>,
|
|
clock: u64,
|
|
dummy: wgpu::TextureView,
|
|
/// Total bytes uploaded (stats).
|
|
pub uploaded_bytes: u64,
|
|
}
|
|
|
|
fn tex_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
|
|
wgpu::BindGroupLayoutEntry {
|
|
binding,
|
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
ty: wgpu::BindingType::Texture {
|
|
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
|
view_dimension: wgpu::TextureViewDimension::D2,
|
|
multisampled: false,
|
|
},
|
|
count: None,
|
|
}
|
|
}
|
|
|
|
/// f32 → IEEE half (round to nearest even), no dependency needed.
|
|
pub fn f32_to_f16(v: f32) -> u16 {
|
|
let x = v.to_bits();
|
|
let sign = ((x >> 16) & 0x8000) as u16;
|
|
let exp = ((x >> 23) & 0xff) as i32;
|
|
let mant = x & 0x7f_ffff;
|
|
if exp == 0xff {
|
|
return sign | 0x7c00 | if mant != 0 { 0x200 } else { 0 };
|
|
}
|
|
let e = exp - 127 + 15;
|
|
if e >= 0x1f {
|
|
return sign | 0x7c00;
|
|
}
|
|
if e <= 0 {
|
|
if e < -10 {
|
|
return sign;
|
|
}
|
|
let m = (mant | 0x80_0000) >> (1 - e);
|
|
let round = (m >> 12) & 1;
|
|
return sign | (((m >> 13) + round) as u16);
|
|
}
|
|
let m = mant >> 13;
|
|
let round_bits = mant & 0x1fff;
|
|
let mut h = (sign as u32) | ((e as u32) << 10) | m;
|
|
if round_bits > 0x1000 || (round_bits == 0x1000 && (m & 1) == 1) {
|
|
h += 1;
|
|
}
|
|
h as u16
|
|
}
|
|
|
|
/// Half-float texel data for one frame (one byte vector per plane), converted off the UI thread.
|
|
/// 8-bit frames need no conversion and have none.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct Prepared {
|
|
planes: Vec<Vec<u8>>,
|
|
}
|
|
|
|
impl Prepared {
|
|
pub fn bytes(&self) -> usize {
|
|
self.planes.iter().map(Vec::len).sum()
|
|
}
|
|
}
|
|
|
|
/// [`Prepared`] data for every layer of a plan (index-aligned with its layers), or for the
|
|
/// fallback image of a [`FramePlan::Image`].
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct PreparedPlan {
|
|
layers: Vec<Option<Prepared>>,
|
|
image: Option<Prepared>,
|
|
}
|
|
|
|
impl PreparedPlan {
|
|
pub fn bytes(&self) -> usize {
|
|
self.layers.iter().flatten().chain(&self.image).map(Prepared::bytes).sum()
|
|
}
|
|
}
|
|
|
|
/// Values per parallel chunk when converting.
|
|
const CHUNK: usize = 1 << 15;
|
|
|
|
fn f32_to_f16_bytes(v: &[f32]) -> Vec<u8> {
|
|
let mut out = vec![0u8; v.len() * 2];
|
|
out.par_chunks_mut(CHUNK * 2).zip(v.par_chunks(CHUNK)).for_each(|(o, s)| {
|
|
for (o, x) in o.chunks_exact_mut(2).zip(s) {
|
|
o.copy_from_slice(&f32_to_f16(*x).to_le_bytes());
|
|
}
|
|
});
|
|
out
|
|
}
|
|
|
|
/// Half floats of `code / 2^bits` for every 16-bit code (built once per bit depth).
|
|
fn code_table(bits: u32) -> &'static [[u8; 2]] {
|
|
static TABLES: [std::sync::OnceLock<Vec<[u8; 2]>>; 17] = [const { std::sync::OnceLock::new() }; 17];
|
|
let bits = bits.min(16);
|
|
TABLES[bits as usize].get_or_init(|| {
|
|
let scale = (1u32 << bits) as f32;
|
|
(0..=u16::MAX as u32).map(|c| f32_to_f16(c as f32 / scale).to_le_bytes()).collect()
|
|
})
|
|
}
|
|
|
|
/// `code / 2^bits` as half floats, through a per-code table (same values as converting each sample).
|
|
fn codes_to_f16_bytes(v: &[u16], bits: u32) -> Vec<u8> {
|
|
let table = code_table(bits);
|
|
let mut out = vec![0u8; v.len() * 2];
|
|
out.par_chunks_mut(CHUNK * 2).zip(v.par_chunks(CHUNK)).for_each(|(o, s)| {
|
|
for (o, c) in o.chunks_exact_mut(2).zip(s) {
|
|
o.copy_from_slice(&table[*c as usize]);
|
|
}
|
|
});
|
|
out
|
|
}
|
|
|
|
/// Convert a frame's texels for upload (None when it uploads as it is).
|
|
pub fn prepare_frame(f: &VideoFrame) -> Option<Prepared> {
|
|
match &f.data {
|
|
PixelData::RgbaF32(d) => Some(Prepared { planes: vec![f32_to_f16_bytes(d)] }),
|
|
PixelData::Yuv16 { planes, bits, .. } => Some(Prepared { planes: planes.iter().map(|p| codes_to_f16_bytes(p, *bits)).collect() }),
|
|
PixelData::Rgba8(_) | PixelData::Yuv8 { .. } => None,
|
|
}
|
|
}
|
|
|
|
/// Convert every layer of a plan for upload. Thread-safe and GPU-free: run it on a worker.
|
|
pub fn prepare(plan: &FramePlan) -> PreparedPlan {
|
|
match plan {
|
|
FramePlan::Layers { layers, .. } => PreparedPlan { layers: layers.iter().map(|l| prepare_frame(&l.frame)).collect(), image: None },
|
|
FramePlan::Image(img) => PreparedPlan { layers: Vec::new(), image: Some(Prepared { planes: vec![f32_to_f16_bytes(&img.px)] }) },
|
|
}
|
|
}
|
|
|
|
impl GpuCompositor {
|
|
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
|
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: Some("filmcraft-composite"),
|
|
source: wgpu::ShaderSource::Wgsl(include_str!("composite.wgsl").into()),
|
|
});
|
|
let layer_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
label: Some("layer"),
|
|
entries: &[
|
|
wgpu::BindGroupLayoutEntry {
|
|
binding: 0,
|
|
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
|
|
ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None },
|
|
count: None,
|
|
},
|
|
tex_entry(1),
|
|
tex_entry(2),
|
|
tex_entry(3),
|
|
],
|
|
});
|
|
let final_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
label: Some("final"),
|
|
entries: &[wgpu::BindGroupLayoutEntry { binding: 0, ..tex_entry(0) }],
|
|
});
|
|
let pl =
|
|
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("layer"), bind_group_layouts: &[Some(&layer_bgl)], immediate_size: 0 });
|
|
let layer_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
label: Some("layer"),
|
|
layout: Some(&pl),
|
|
vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs"), compilation_options: Default::default(), buffers: &[] },
|
|
primitive: wgpu::PrimitiveState::default(),
|
|
depth_stencil: None,
|
|
multisample: wgpu::MultisampleState::default(),
|
|
fragment: Some(wgpu::FragmentState {
|
|
module: &shader,
|
|
entry_point: Some("fs"),
|
|
compilation_options: Default::default(),
|
|
targets: &[Some(wgpu::ColorTargetState {
|
|
format: ACCUM_FORMAT,
|
|
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
|
|
write_mask: wgpu::ColorWrites::ALL,
|
|
})],
|
|
}),
|
|
multiview_mask: None,
|
|
cache: None,
|
|
});
|
|
let fpl =
|
|
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("final"), bind_group_layouts: &[Some(&final_bgl)], immediate_size: 0 });
|
|
let final_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
label: Some("final"),
|
|
layout: Some(&fpl),
|
|
vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs_full"), compilation_options: Default::default(), buffers: &[] },
|
|
primitive: wgpu::PrimitiveState::default(),
|
|
depth_stencil: None,
|
|
multisample: wgpu::MultisampleState::default(),
|
|
fragment: Some(wgpu::FragmentState {
|
|
module: &shader,
|
|
entry_point: Some("fs_full"),
|
|
compilation_options: Default::default(),
|
|
targets: &[Some(wgpu::ColorTargetState { format: OUTPUT_FORMAT, blend: None, write_mask: wgpu::ColorWrites::ALL })],
|
|
}),
|
|
multiview_mask: None,
|
|
cache: None,
|
|
});
|
|
let dummy_tex = device.create_texture(&wgpu::TextureDescriptor {
|
|
label: Some("dummy"),
|
|
size: wgpu::Extent3d { width: 1, height: 1, depth_or_array_layers: 1 },
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: wgpu::TextureDimension::D2,
|
|
format: wgpu::TextureFormat::R8Unorm,
|
|
usage: wgpu::TextureUsages::TEXTURE_BINDING,
|
|
view_formats: &[],
|
|
});
|
|
let dummy = dummy_tex.create_view(&Default::default());
|
|
Self {
|
|
device: device.clone(),
|
|
queue: queue.clone(),
|
|
layer_pipeline,
|
|
final_pipeline,
|
|
layer_bgl,
|
|
final_bgl,
|
|
accum: None,
|
|
output: None,
|
|
uploads: HashMap::new(),
|
|
clock: 0,
|
|
dummy,
|
|
uploaded_bytes: 0,
|
|
}
|
|
}
|
|
|
|
fn target(
|
|
device: &wgpu::Device,
|
|
slot: &mut Option<(wgpu::Texture, wgpu::TextureView, (u32, u32))>,
|
|
w: u32,
|
|
h: u32,
|
|
format: wgpu::TextureFormat,
|
|
extra: wgpu::TextureUsages,
|
|
) {
|
|
if slot.as_ref().is_some_and(|s| s.2 == (w, h)) {
|
|
return;
|
|
}
|
|
let t = device.create_texture(&wgpu::TextureDescriptor {
|
|
label: Some("filmcraft-target"),
|
|
size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: wgpu::TextureDimension::D2,
|
|
format,
|
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING | extra,
|
|
view_formats: &[],
|
|
});
|
|
let v = t.create_view(&Default::default());
|
|
*slot = Some((t, v, (w, h)));
|
|
}
|
|
|
|
fn plane_texture(&mut self, w: u32, h: u32, format: wgpu::TextureFormat, bytes: &[u8], bpp: u32) -> wgpu::TextureView {
|
|
let t = self.device.create_texture(&wgpu::TextureDescriptor {
|
|
label: Some("filmcraft-plane"),
|
|
size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: wgpu::TextureDimension::D2,
|
|
format,
|
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
|
view_formats: &[],
|
|
});
|
|
self.queue.write_texture(
|
|
wgpu::TexelCopyTextureInfo { texture: &t, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
|
|
bytes,
|
|
wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(w * bpp), rows_per_image: Some(h) },
|
|
wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 },
|
|
);
|
|
self.uploaded_bytes += bytes.len() as u64;
|
|
t.create_view(&Default::default())
|
|
}
|
|
|
|
/// Upload (or reuse) the textures of a frame, using `prep` when it was converted beforehand.
|
|
fn upload(&mut self, f: &VideoFrame, prep: Option<&Prepared>) -> (usize, u32, u32) {
|
|
let id = match &f.data {
|
|
PixelData::Rgba8(d) => Arc::as_ptr(d) as *const u8 as usize,
|
|
PixelData::RgbaF32(d) => Arc::as_ptr(d) as *const u8 as usize,
|
|
PixelData::Yuv8 { planes, .. } => Arc::as_ptr(&planes[0]) as *const u8 as usize,
|
|
PixelData::Yuv16 { planes, .. } => Arc::as_ptr(&planes[0]) as *const u8 as usize,
|
|
};
|
|
let key = (id, f.width, f.height);
|
|
self.clock += 1;
|
|
if let Some(u) = self.uploads.get_mut(&key) {
|
|
u.last_used = self.clock;
|
|
return key;
|
|
}
|
|
let (w, h) = (f.width, f.height);
|
|
let d0 = self.dummy.clone();
|
|
let dummy = || d0.clone();
|
|
let up = match &f.data {
|
|
PixelData::Rgba8(d) => {
|
|
let v = self.plane_texture(w, h, wgpu::TextureFormat::Rgba8UnormSrgb, d, 4);
|
|
Uploaded { views: [v, dummy(), dummy()], kind: 0, code_scale: 1.0, chroma: (w, h), last_used: self.clock, _pixels: f.data.clone() }
|
|
}
|
|
PixelData::RgbaF32(d) => {
|
|
let owned;
|
|
let half = match prep {
|
|
Some(p) => &p.planes[0],
|
|
None => {
|
|
owned = f32_to_f16_bytes(d);
|
|
&owned
|
|
}
|
|
};
|
|
let v = self.plane_texture(w, h, wgpu::TextureFormat::Rgba16Float, half, 8);
|
|
Uploaded { views: [v, dummy(), dummy()], kind: 1, code_scale: 1.0, chroma: (w, h), last_used: self.clock, _pixels: f.data.clone() }
|
|
}
|
|
PixelData::Yuv8 { planes, chroma, .. } => {
|
|
let (sx, sy) = chroma.shifts();
|
|
let (cw, ch) = (w.div_ceil(1 << sx), h.div_ceil(1 << sy));
|
|
let y = self.plane_texture(w, h, wgpu::TextureFormat::R8Unorm, &planes[0], 1);
|
|
let u = self.plane_texture(cw, ch, wgpu::TextureFormat::R8Unorm, &planes[1], 1);
|
|
let v = self.plane_texture(cw, ch, wgpu::TextureFormat::R8Unorm, &planes[2], 1);
|
|
Uploaded { views: [y, u, v], kind: 2, code_scale: 255.0, chroma: (cw, ch), last_used: self.clock, _pixels: f.data.clone() }
|
|
}
|
|
PixelData::Yuv16 { planes, chroma, bits, .. } => {
|
|
let (sx, sy) = chroma.shifts();
|
|
let (cw, ch) = (w.div_ceil(1 << sx), h.div_ceil(1 << sy));
|
|
let scale = (1u32 << bits) as f32;
|
|
let owned;
|
|
let p = match prep {
|
|
Some(p) => p,
|
|
None => {
|
|
owned = Prepared { planes: planes.iter().map(|p| codes_to_f16_bytes(p, *bits)).collect() };
|
|
&owned
|
|
}
|
|
};
|
|
let y = self.plane_texture(w, h, wgpu::TextureFormat::R16Float, &p.planes[0], 2);
|
|
let u = self.plane_texture(cw, ch, wgpu::TextureFormat::R16Float, &p.planes[1], 2);
|
|
let v = self.plane_texture(cw, ch, wgpu::TextureFormat::R16Float, &p.planes[2], 2);
|
|
let _ = Chroma::C420;
|
|
Uploaded { views: [y, u, v], kind: 2, code_scale: scale, chroma: (cw, ch), last_used: self.clock, _pixels: f.data.clone() }
|
|
}
|
|
};
|
|
self.uploads.insert(key, up);
|
|
// keep the most recent uploads (enough for several stacked layers + playback lookahead)
|
|
if self.uploads.len() > 24 {
|
|
let mut v: Vec<(u64, (usize, u32, u32))> = self.uploads.iter().map(|(k, u)| (u.last_used, *k)).collect();
|
|
v.sort_unstable();
|
|
for (_, k) in v.into_iter().take(self.uploads.len() - 24) {
|
|
self.uploads.remove(&k);
|
|
}
|
|
}
|
|
key
|
|
}
|
|
|
|
fn uniforms(&self, l: &PlanLayer, key: (usize, u32, u32), out: (u32, u32)) -> [f32; 24] {
|
|
let up = &self.uploads[&key];
|
|
let f = &l.frame;
|
|
let m = &l.matrix;
|
|
let (kr, kb) = f.color.matrix.kr_kb();
|
|
let bits_scale = up.code_scale / 255.0; // code units relative to 8-bit
|
|
let (yo, ys, co, cs) = match (f.color.range, up.kind) {
|
|
(Range::Limited, 2) => (16.0 * bits_scale, 219.0 * bits_scale, 128.0 * bits_scale, 224.0 * bits_scale),
|
|
(Range::Full, 2) => (0.0, up.code_scale - 1.0, up.code_scale / 2.0, up.code_scale - 1.0),
|
|
_ => (0.0, 1.0, 0.0, 1.0),
|
|
};
|
|
let transfer = match f.color.transfer {
|
|
Transfer::Linear => 1.0,
|
|
Transfer::Pq => 2.0,
|
|
Transfer::Hlg => 3.0,
|
|
_ => 0.0,
|
|
};
|
|
// source pixels per output pixel → supersampling taps
|
|
let sx = (m.a * m.a + m.b * m.b).sqrt();
|
|
let sy = (m.c * m.c + m.d * m.d).sqrt();
|
|
let footprint = (1.0 / sx.min(sy).max(1e-6)) as f32;
|
|
let taps = if footprint > 1.25 { footprint.ceil().min(4.0) } else { 1.0 };
|
|
let _ = Matrix::Bt709;
|
|
[
|
|
m.a as f32,
|
|
m.b as f32,
|
|
m.c as f32,
|
|
m.d as f32,
|
|
m.e as f32,
|
|
m.f as f32,
|
|
out.0 as f32,
|
|
out.1 as f32,
|
|
f.width as f32,
|
|
f.height as f32,
|
|
up.chroma.0 as f32,
|
|
up.chroma.1 as f32,
|
|
l.opacity,
|
|
up.kind as f32,
|
|
taps,
|
|
transfer,
|
|
yo,
|
|
ys,
|
|
co,
|
|
cs,
|
|
kr,
|
|
kb,
|
|
up.code_scale,
|
|
footprint.max(1.0),
|
|
]
|
|
}
|
|
|
|
/// Composite a plan; returns the output view (sRGB, over black) and its size.
|
|
pub fn composite(&mut self, plan: &FramePlan) -> (&wgpu::TextureView, (u32, u32)) {
|
|
self.composite_prepared(plan, None)
|
|
}
|
|
|
|
/// [`composite`](Self::composite) with texel conversions already done by [`prepare`] (the
|
|
/// result is identical; only the upload work on this thread differs).
|
|
pub fn composite_prepared(&mut self, plan: &FramePlan, prep: Option<&PreparedPlan>) -> (&wgpu::TextureView, (u32, u32)) {
|
|
let owned;
|
|
let (w, h, layers): (u32, u32, &[PlanLayer]) = match plan {
|
|
FramePlan::Layers { width, height, layers } => (*width as u32, *height as u32, layers.as_slice()),
|
|
FramePlan::Image(img) => {
|
|
let frame = match prep.and_then(|p| p.image.as_ref()) {
|
|
// The texels come from `prep`; the frame only carries size and colour.
|
|
Some(_) => VideoFrame {
|
|
width: img.w as u32,
|
|
height: img.h as u32,
|
|
data: PixelData::RgbaF32(Arc::new(Vec::new())),
|
|
..VideoFrame::rgba_f32(1, 1, vec![0.0; 4])
|
|
},
|
|
None => VideoFrame::rgba_f32(img.w as u32, img.h as u32, img.px.clone()),
|
|
};
|
|
owned = [PlanLayer { frame: Arc::new(frame), matrix: filmcraft_geom::Affine::IDENTITY, opacity: 1.0 }];
|
|
(img.w as u32, img.h as u32, &owned[..])
|
|
}
|
|
};
|
|
let (w, h) = (w.max(1), h.max(1));
|
|
Self::target(&self.device, &mut self.accum, w, h, ACCUM_FORMAT, wgpu::TextureUsages::empty());
|
|
Self::target(&self.device, &mut self.output, w, h, OUTPUT_FORMAT, wgpu::TextureUsages::COPY_SRC);
|
|
let keys: Vec<(usize, u32, u32)> = layers
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, l)| {
|
|
let p = prep.and_then(|p| if matches!(plan, FramePlan::Image(_)) { p.image.as_ref() } else { p.layers.get(i).and_then(Option::as_ref) });
|
|
self.upload(&l.frame, p)
|
|
})
|
|
.collect();
|
|
let mut bind_groups = Vec::with_capacity(layers.len());
|
|
for (l, k) in layers.iter().zip(&keys) {
|
|
let u = self.uniforms(l, *k, (w, h));
|
|
let bytes: Vec<u8> = u.iter().flat_map(|v| v.to_le_bytes()).collect();
|
|
let buf = self.device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("layer-u"),
|
|
size: bytes.len() as u64,
|
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
});
|
|
self.queue.write_buffer(&buf, 0, &bytes);
|
|
let up = &self.uploads[k];
|
|
let bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("layer"),
|
|
layout: &self.layer_bgl,
|
|
entries: &[
|
|
wgpu::BindGroupEntry { binding: 0, resource: buf.as_entire_binding() },
|
|
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&up.views[0]) },
|
|
wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&up.views[1]) },
|
|
wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::TextureView(&up.views[2]) },
|
|
],
|
|
});
|
|
bind_groups.push(bg);
|
|
}
|
|
let accum_view = &self.accum.as_ref().expect("accum").1;
|
|
let out_view = &self.output.as_ref().expect("output").1;
|
|
let final_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: Some("final"),
|
|
layout: &self.final_bgl,
|
|
entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(accum_view) }],
|
|
});
|
|
let mut enc = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("filmcraft-composite") });
|
|
{
|
|
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
label: Some("layers"),
|
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|
view: accum_view,
|
|
depth_slice: None,
|
|
resolve_target: None,
|
|
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store },
|
|
})],
|
|
depth_stencil_attachment: None,
|
|
timestamp_writes: None,
|
|
occlusion_query_set: None,
|
|
multiview_mask: None,
|
|
});
|
|
pass.set_pipeline(&self.layer_pipeline);
|
|
for bg in &bind_groups {
|
|
pass.set_bind_group(0, bg, &[]);
|
|
pass.draw(0..6, 0..1);
|
|
}
|
|
}
|
|
{
|
|
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
|
|
label: Some("resolve"),
|
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|
view: out_view,
|
|
depth_slice: None,
|
|
resolve_target: None,
|
|
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store },
|
|
})],
|
|
depth_stencil_attachment: None,
|
|
timestamp_writes: None,
|
|
occlusion_query_set: None,
|
|
multiview_mask: None,
|
|
});
|
|
pass.set_pipeline(&self.final_pipeline);
|
|
pass.set_bind_group(0, &final_bg, &[]);
|
|
pass.draw(0..3, 0..1);
|
|
}
|
|
self.queue.submit([enc.finish()]);
|
|
(&self.output.as_ref().expect("output").1, (w, h))
|
|
}
|
|
|
|
/// Read the output back as RGBA8 (tests / screenshots / thumbnails).
|
|
pub fn read_output(&self) -> Option<(u32, u32, Vec<u8>)> {
|
|
let (tex, _, (w, h)) = self.output.as_ref()?;
|
|
let row = (w * 4).div_ceil(256) * 256;
|
|
let buf = self.device.create_buffer(&wgpu::BufferDescriptor {
|
|
label: Some("readback"),
|
|
size: (row * h) as u64,
|
|
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
|
mapped_at_creation: false,
|
|
});
|
|
let mut enc = self.device.create_command_encoder(&Default::default());
|
|
enc.copy_texture_to_buffer(
|
|
wgpu::TexelCopyTextureInfo { texture: tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
|
|
wgpu::TexelCopyBufferInfo { buffer: &buf, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(row), rows_per_image: Some(*h) } },
|
|
wgpu::Extent3d { width: *w, height: *h, depth_or_array_layers: 1 },
|
|
);
|
|
self.queue.submit([enc.finish()]);
|
|
let slice = buf.slice(..);
|
|
let (tx, rx) = std::sync::mpsc::channel();
|
|
slice.map_async(wgpu::MapMode::Read, move |r| {
|
|
let _ = tx.send(r);
|
|
});
|
|
let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
|
|
rx.recv().ok()?.ok()?;
|
|
let data = slice.get_mapped_range().ok()?;
|
|
let mut out = Vec::with_capacity((w * h * 4) as usize);
|
|
for y in 0..*h {
|
|
out.extend_from_slice(&data[(y * row) as usize..(y * row + w * 4) as usize]);
|
|
}
|
|
drop(data);
|
|
buf.unmap();
|
|
Some((*w, *h, out))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|