1use bytemuck::{Pod, Zeroable};
40
41use crate::shaders::SCALED_BLIT_WGSL;
42use crate::WgpuGfxCtx;
43
44const DST_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub struct RectInPixels {
56 pub x: u32,
57 pub y: u32,
58 pub width: u32,
59 pub height: u32,
60}
61
62impl RectInPixels {
63 pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
64 Self {
65 x,
66 y,
67 width,
68 height,
69 }
70 }
71}
72
73pub(crate) fn clamp_region(
80 region: Option<RectInPixels>,
81 src_w: u32,
82 src_h: u32,
83) -> Option<RectInPixels> {
84 if src_w == 0 || src_h == 0 {
85 return None;
86 }
87 let Some(r) = region else {
88 return Some(RectInPixels::new(0, 0, src_w, src_h));
89 };
90 if r.width == 0 || r.height == 0 || r.x >= src_w || r.y >= src_h {
91 return None;
92 }
93 let width = r.width.min(src_w - r.x);
94 let height = r.height.min(src_h - r.y);
95 Some(RectInPixels::new(r.x, r.y, width, height))
96}
97
98pub(crate) fn padded_bytes_per_row(w: u32) -> u32 {
103 const ALIGN: u32 = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
104 (w * 4).div_ceil(ALIGN) * ALIGN
105}
106
107#[repr(C)]
110#[derive(Clone, Copy, Debug, Pod, Zeroable)]
111struct ScaledBlitUniforms {
112 uv_offset: [f32; 2],
113 uv_scale: [f32; 2],
114 flags: [f32; 4],
115}
116const _: () = assert!(std::mem::size_of::<ScaledBlitUniforms>() == 32);
117
118pub(crate) struct ScaledBlit {
124 pipeline: wgpu::RenderPipeline,
125 bgl0: wgpu::BindGroupLayout,
126 bgl1: wgpu::BindGroupLayout,
127 sampler: wgpu::Sampler,
128 dst: Option<(wgpu::Texture, wgpu::TextureView, u32, u32)>,
129}
130
131impl ScaledBlit {
132 fn new(device: &wgpu::Device) -> Self {
133 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
134 label: Some("scaled_blit"),
135 source: wgpu::ShaderSource::Wgsl(SCALED_BLIT_WGSL.into()),
136 });
137 let bgl0 = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
138 label: Some("scaled_blit_bgl0"),
139 entries: &[wgpu::BindGroupLayoutEntry {
140 binding: 0,
141 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
142 ty: wgpu::BindingType::Buffer {
143 ty: wgpu::BufferBindingType::Uniform,
144 has_dynamic_offset: false,
145 min_binding_size: wgpu::BufferSize::new(
146 std::mem::size_of::<ScaledBlitUniforms>() as u64,
147 ),
148 },
149 count: None,
150 }],
151 });
152 let bgl1 = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
153 label: Some("scaled_blit_bgl1"),
154 entries: &[
155 wgpu::BindGroupLayoutEntry {
156 binding: 0,
157 visibility: wgpu::ShaderStages::FRAGMENT,
158 ty: wgpu::BindingType::Texture {
159 sample_type: wgpu::TextureSampleType::Float { filterable: true },
160 view_dimension: wgpu::TextureViewDimension::D2,
161 multisampled: false,
162 },
163 count: None,
164 },
165 wgpu::BindGroupLayoutEntry {
166 binding: 1,
167 visibility: wgpu::ShaderStages::FRAGMENT,
168 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
169 count: None,
170 },
171 ],
172 });
173 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
174 label: Some("scaled_blit_layout"),
175 bind_group_layouts: &[Some(&bgl0), Some(&bgl1)],
176 immediate_size: 0,
177 });
178 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
179 label: Some("scaled_blit"),
180 layout: Some(&layout),
181 vertex: wgpu::VertexState {
182 module: &shader,
183 entry_point: Some("vs_main"),
184 buffers: &[],
185 compilation_options: Default::default(),
186 },
187 fragment: Some(wgpu::FragmentState {
188 module: &shader,
189 entry_point: Some("fs_main"),
190 targets: &[Some(wgpu::ColorTargetState {
191 format: DST_FORMAT,
192 blend: None,
195 write_mask: wgpu::ColorWrites::ALL,
196 })],
197 compilation_options: Default::default(),
198 }),
199 primitive: wgpu::PrimitiveState::default(),
200 depth_stencil: None,
201 multisample: wgpu::MultisampleState::default(),
202 multiview_mask: None,
203 cache: None,
204 });
205 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
206 label: Some("scaled_blit_sampler"),
207 address_mode_u: wgpu::AddressMode::ClampToEdge,
208 address_mode_v: wgpu::AddressMode::ClampToEdge,
209 address_mode_w: wgpu::AddressMode::ClampToEdge,
210 mag_filter: wgpu::FilterMode::Linear,
211 min_filter: wgpu::FilterMode::Linear,
212 mipmap_filter: wgpu::MipmapFilterMode::Linear,
213 ..Default::default()
214 });
215 Self {
216 pipeline,
217 bgl0,
218 bgl1,
219 sampler,
220 dst: None,
221 }
222 }
223
224 fn ensure_dst(&mut self, device: &wgpu::Device, w: u32, h: u32) -> &wgpu::TextureView {
228 let need_alloc = match &self.dst {
229 Some((_, _, dw, dh)) => *dw != w || *dh != h,
230 None => true,
231 };
232 if need_alloc {
233 let texture = device.create_texture(&wgpu::TextureDescriptor {
234 label: Some("scaled_capture_dst"),
235 size: wgpu::Extent3d {
236 width: w,
237 height: h,
238 depth_or_array_layers: 1,
239 },
240 mip_level_count: 1,
241 sample_count: 1,
242 dimension: wgpu::TextureDimension::D2,
243 format: DST_FORMAT,
244 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
245 view_formats: &[],
246 });
247 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
248 self.dst = Some((texture, view, w, h));
249 }
250 &self.dst.as_ref().expect("dst texture just allocated").1
252 }
253}
254
255impl WgpuGfxCtx {
256 pub fn has_pending_scaled_readback(&self) -> bool {
259 self.pending_scaled_readback.is_some()
260 }
261
262 pub fn begin_capture_readback_scaled(
280 &mut self,
281 src_region: Option<RectInPixels>,
282 dst_w: u32,
283 dst_h: u32,
284 ) -> bool {
285 if self.pending_scaled_readback.is_some() || dst_w == 0 || dst_h == 0 {
286 return false;
287 }
288 let Some((_, src_view, src_w, src_h)) = self.capture_texture.as_ref() else {
289 return false;
290 };
291 let (src_view, src_w, src_h) = (src_view.clone(), *src_w, *src_h);
292 let Some(region) = clamp_region(src_region, src_w, src_h) else {
293 return false;
294 };
295
296 if self.scaled_blit.is_none() {
297 self.scaled_blit = Some(ScaledBlit::new(&self.device));
298 }
299 let device = std::sync::Arc::clone(&self.device);
300 let srgb_src = matches!(
301 self.surface_format,
302 wgpu::TextureFormat::Bgra8UnormSrgb | wgpu::TextureFormat::Rgba8UnormSrgb
303 );
304 let uniforms = ScaledBlitUniforms {
305 uv_offset: [
306 region.x as f32 / src_w as f32,
307 region.y as f32 / src_h as f32,
308 ],
309 uv_scale: [
310 region.width as f32 / src_w as f32,
311 region.height as f32 / src_h as f32,
312 ],
313 flags: [if srgb_src { 1.0 } else { 0.0 }, 0.0, 0.0, 0.0],
314 };
315
316 let blit = self
317 .scaled_blit
318 .as_mut()
319 .expect("scaled blit resources just created");
320 let dst_view = blit.ensure_dst(&device, dst_w, dst_h).clone();
321 let ub = device.create_buffer(&wgpu::BufferDescriptor {
322 label: Some("scaled_blit_uniforms"),
323 size: std::mem::size_of::<ScaledBlitUniforms>() as u64,
324 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
325 mapped_at_creation: false,
326 });
327 self.queue
328 .write_buffer(&ub, 0, bytemuck::bytes_of(&uniforms));
329 let bg0 = device.create_bind_group(&wgpu::BindGroupDescriptor {
330 label: Some("scaled_blit_bg0"),
331 layout: &blit.bgl0,
332 entries: &[wgpu::BindGroupEntry {
333 binding: 0,
334 resource: ub.as_entire_binding(),
335 }],
336 });
337 let bg1 = device.create_bind_group(&wgpu::BindGroupDescriptor {
338 label: Some("scaled_blit_bg1"),
339 layout: &blit.bgl1,
340 entries: &[
341 wgpu::BindGroupEntry {
342 binding: 0,
343 resource: wgpu::BindingResource::TextureView(&src_view),
344 },
345 wgpu::BindGroupEntry {
346 binding: 1,
347 resource: wgpu::BindingResource::Sampler(&blit.sampler),
348 },
349 ],
350 });
351
352 let padded_bpr = padded_bytes_per_row(dst_w);
353 let staging = device.create_buffer(&wgpu::BufferDescriptor {
354 label: Some("scaled_capture_readback"),
355 size: (padded_bpr as u64) * (dst_h as u64),
356 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
357 mapped_at_creation: false,
358 });
359
360 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
361 label: Some("scaled_capture_blit"),
362 });
363 {
364 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
365 label: Some("scaled_capture_pass"),
366 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
367 view: &dst_view,
368 depth_slice: None,
369 resolve_target: None,
370 ops: wgpu::Operations {
371 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
372 store: wgpu::StoreOp::Store,
373 },
374 })],
375 depth_stencil_attachment: None,
376 timestamp_writes: None,
377 occlusion_query_set: None,
378 multiview_mask: None,
379 });
380 pass.set_pipeline(&blit.pipeline);
381 pass.set_bind_group(0, &bg0, &[]);
382 pass.set_bind_group(1, &bg1, &[]);
383 pass.draw(0..3, 0..1);
385 }
386 let dst_texture = &blit.dst.as_ref().expect("dst texture ensured above").0;
387 encoder.copy_texture_to_buffer(
388 wgpu::TexelCopyTextureInfo {
389 texture: dst_texture,
390 mip_level: 0,
391 origin: wgpu::Origin3d::ZERO,
392 aspect: wgpu::TextureAspect::All,
393 },
394 wgpu::TexelCopyBufferInfo {
395 buffer: &staging,
396 layout: wgpu::TexelCopyBufferLayout {
397 offset: 0,
398 bytes_per_row: Some(padded_bpr),
399 rows_per_image: Some(dst_h),
400 },
401 },
402 wgpu::Extent3d {
403 width: dst_w,
404 height: dst_h,
405 depth_or_array_layers: 1,
406 },
407 );
408 self.queue.submit(std::iter::once(encoder.finish()));
409
410 let (tx, rx) = std::sync::mpsc::channel();
411 staging
412 .slice(..)
413 .map_async(wgpu::MapMode::Read, move |res| {
414 let _ = tx.send(res);
415 });
416 self.pending_scaled_readback = Some(crate::PendingReadback {
417 staging,
418 w: dst_w,
419 h: dst_h,
420 padded_bpr,
421 done: rx,
422 });
423 true
424 }
425
426 pub fn poll_capture_readback_scaled(&mut self) -> Option<(Vec<u8>, u32, u32)> {
433 let _ = self.device.poll(wgpu::PollType::Poll);
434 match self.pending_scaled_readback.as_ref()?.done.try_recv() {
435 Ok(Ok(())) => {}
436 Err(std::sync::mpsc::TryRecvError::Empty) => return None, Ok(Err(_)) | Err(std::sync::mpsc::TryRecvError::Disconnected) => {
438 self.pending_scaled_readback = None; return None;
440 }
441 }
442
443 let crate::PendingReadback {
444 staging,
445 w,
446 h,
447 padded_bpr,
448 ..
449 } = self.pending_scaled_readback.take()?;
450
451 let unpadded_bpr = (w * 4) as usize;
452 let mut out = Vec::with_capacity((w as usize) * (h as usize) * 4);
453 {
454 let view = staging.slice(..).get_mapped_range();
455 for row in 0..h as usize {
456 let start = row * padded_bpr as usize;
457 out.extend_from_slice(&view[start..start + unpadded_bpr]);
458 }
459 }
460 staging.unmap();
461 Some((out, w, h))
462 }
463}