filmcraft/crates/gpu/src/lib.rs

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;