use bytemuck::{Pod, Zeroable};
use crate::shaders::SCALED_BLIT_WGSL;
use crate::WgpuGfxCtx;
const DST_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RectInPixels {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl RectInPixels {
pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
Self {
x,
y,
width,
height,
}
}
}
pub(crate) fn clamp_region(
region: Option<RectInPixels>,
src_w: u32,
src_h: u32,
) -> Option<RectInPixels> {
if src_w == 0 || src_h == 0 {
return None;
}
let Some(r) = region else {
return Some(RectInPixels::new(0, 0, src_w, src_h));
};
if r.width == 0 || r.height == 0 || r.x >= src_w || r.y >= src_h {
return None;
}
let width = r.width.min(src_w - r.x);
let height = r.height.min(src_h - r.y);
Some(RectInPixels::new(r.x, r.y, width, height))
}
pub(crate) fn padded_bytes_per_row(w: u32) -> u32 {
const ALIGN: u32 = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
(w * 4).div_ceil(ALIGN) * ALIGN
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
struct ScaledBlitUniforms {
uv_offset: [f32; 2],
uv_scale: [f32; 2],
flags: [f32; 4],
}
const _: () = assert!(std::mem::size_of::<ScaledBlitUniforms>() == 32);
pub(crate) struct ScaledBlit {
pipeline: wgpu::RenderPipeline,
bgl0: wgpu::BindGroupLayout,
bgl1: wgpu::BindGroupLayout,
sampler: wgpu::Sampler,
dst: Option<(wgpu::Texture, wgpu::TextureView, u32, u32)>,
}
impl ScaledBlit {
fn new(device: &wgpu::Device) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("scaled_blit"),
source: wgpu::ShaderSource::Wgsl(SCALED_BLIT_WGSL.into()),
});
let bgl0 = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("scaled_blit_bgl0"),
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: wgpu::BufferSize::new(
std::mem::size_of::<ScaledBlitUniforms>() as u64,
),
},
count: None,
}],
});
let bgl1 = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("scaled_blit_bgl1"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("scaled_blit_layout"),
bind_group_layouts: &[Some(&bgl0), Some(&bgl1)],
immediate_size: 0,
});
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("scaled_blit"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: Default::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: DST_FORMAT,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("scaled_blit_sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::MipmapFilterMode::Linear,
..Default::default()
});
Self {
pipeline,
bgl0,
bgl1,
sampler,
dst: None,
}
}
fn ensure_dst(&mut self, device: &wgpu::Device, w: u32, h: u32) -> &wgpu::TextureView {
let need_alloc = match &self.dst {
Some((_, _, dw, dh)) => *dw != w || *dh != h,
None => true,
};
if need_alloc {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("scaled_capture_dst"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: DST_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
self.dst = Some((texture, view, w, h));
}
&self.dst.as_ref().expect("dst texture just allocated").1
}
}
impl WgpuGfxCtx {
pub fn has_pending_scaled_readback(&self) -> bool {
self.pending_scaled_readback.is_some()
}
pub fn begin_capture_readback_scaled(
&mut self,
src_region: Option<RectInPixels>,
dst_w: u32,
dst_h: u32,
) -> bool {
if self.pending_scaled_readback.is_some() || dst_w == 0 || dst_h == 0 {
return false;
}
let Some((_, src_view, src_w, src_h)) = self.capture_texture.as_ref() else {
return false;
};
let (src_view, src_w, src_h) = (src_view.clone(), *src_w, *src_h);
let Some(region) = clamp_region(src_region, src_w, src_h) else {
return false;
};
if self.scaled_blit.is_none() {
self.scaled_blit = Some(ScaledBlit::new(&self.device));
}
let device = std::sync::Arc::clone(&self.device);
let srgb_src = matches!(
self.surface_format,
wgpu::TextureFormat::Bgra8UnormSrgb | wgpu::TextureFormat::Rgba8UnormSrgb
);
let uniforms = ScaledBlitUniforms {
uv_offset: [
region.x as f32 / src_w as f32,
region.y as f32 / src_h as f32,
],
uv_scale: [
region.width as f32 / src_w as f32,
region.height as f32 / src_h as f32,
],
flags: [if srgb_src { 1.0 } else { 0.0 }, 0.0, 0.0, 0.0],
};
let blit = self
.scaled_blit
.as_mut()
.expect("scaled blit resources just created");
let dst_view = blit.ensure_dst(&device, dst_w, dst_h).clone();
let ub = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("scaled_blit_uniforms"),
size: std::mem::size_of::<ScaledBlitUniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.queue
.write_buffer(&ub, 0, bytemuck::bytes_of(&uniforms));
let bg0 = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("scaled_blit_bg0"),
layout: &blit.bgl0,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: ub.as_entire_binding(),
}],
});
let bg1 = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("scaled_blit_bg1"),
layout: &blit.bgl1,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&src_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&blit.sampler),
},
],
});
let padded_bpr = padded_bytes_per_row(dst_w);
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("scaled_capture_readback"),
size: (padded_bpr as u64) * (dst_h as u64),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("scaled_capture_blit"),
});
{
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("scaled_capture_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &dst_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(&blit.pipeline);
pass.set_bind_group(0, &bg0, &[]);
pass.set_bind_group(1, &bg1, &[]);
pass.draw(0..3, 0..1);
}
let dst_texture = &blit.dst.as_ref().expect("dst texture ensured above").0;
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: dst_texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &staging,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_bpr),
rows_per_image: Some(dst_h),
},
},
wgpu::Extent3d {
width: dst_w,
height: dst_h,
depth_or_array_layers: 1,
},
);
self.queue.submit(std::iter::once(encoder.finish()));
let (tx, rx) = std::sync::mpsc::channel();
staging
.slice(..)
.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
self.pending_scaled_readback = Some(crate::PendingReadback {
staging,
w: dst_w,
h: dst_h,
padded_bpr,
done: rx,
});
true
}
pub fn poll_capture_readback_scaled(&mut self) -> Option<(Vec<u8>, u32, u32)> {
let _ = self.device.poll(wgpu::PollType::Poll);
match self.pending_scaled_readback.as_ref()?.done.try_recv() {
Ok(Ok(())) => {}
Err(std::sync::mpsc::TryRecvError::Empty) => return None, Ok(Err(_)) | Err(std::sync::mpsc::TryRecvError::Disconnected) => {
self.pending_scaled_readback = None; return None;
}
}
let crate::PendingReadback {
staging,
w,
h,
padded_bpr,
..
} = self.pending_scaled_readback.take()?;
let unpadded_bpr = (w * 4) as usize;
let mut out = Vec::with_capacity((w as usize) * (h as usize) * 4);
{
let view = staging.slice(..).get_mapped_range();
for row in 0..h as usize {
let start = row * padded_bpr as usize;
out.extend_from_slice(&view[start..start + unpadded_bpr]);
}
}
staging.unmap();
Some((out, w, h))
}
}