1use std::sync::atomic::{AtomicBool, Ordering};
2#[cfg(not(target_arch = "wasm32"))]
3use std::sync::mpsc;
4
5use crate::canary::{CANARY_BYTES, Canary};
6use crate::{
7 ReadbackFailure, RenderError, RenderSize, RenderedTexture, Renderer,
8 pipeline::{CachedPipeline, CachedYuvReadbackPipeline},
9 pool::{ReadbackBufferLease, TextureLease},
10 uniform::{UniformArenaLease, f32_bytes},
11};
12use alpha2_gpu::GpuContext;
13use alpha2_media::{ChromaLocation, ColorMatrix, PixelLayout, YuvEncoding};
14use alpha2_shaders::{READBACK_PACK_WGSL, RGB_TO_YUV420_WGSL};
15
16const RGBA8_BYTES_PER_PIXEL: u32 = 4;
17const RGBA16F_BYTES_PER_PIXEL: u32 = 8;
18const YUV420_PLANE_COUNT: u64 = 3;
19
20const PLANE_PLACEMENT_ALIGNMENT: u64 = 512;
23
24#[derive(Copy, Clone, Debug, Eq, PartialEq)]
29pub(crate) struct Yuv420StagingLayout {
30 luma_pitch: u32,
31 chroma_pitch: u32,
32 u_offset: u64,
33 v_offset: u64,
34 bytes: u64,
35}
36
37impl Yuv420StagingLayout {
38 pub(crate) fn new(size: RenderSize) -> Result<Self, RenderError> {
39 let luma_pitch = aligned_copy_row(size.width)?;
40 let chroma_pitch = aligned_copy_row(size.width / 2)?;
41 let region = |pitch: u32, rows: u32| u64::from(pitch).checked_mul(u64::from(rows));
42 let place = |end: u64| end.checked_next_multiple_of(PLANE_PLACEMENT_ALIGNMENT);
43 let chroma_bytes = region(chroma_pitch, size.height / 2)
44 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
45 let u_offset = region(luma_pitch, size.height)
46 .and_then(place)
47 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
48 let v_offset = u_offset
49 .checked_add(chroma_bytes)
50 .and_then(place)
51 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
52 let bytes = v_offset
53 .checked_add(chroma_bytes)
54 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
55 Ok(Self {
56 luma_pitch,
57 chroma_pitch,
58 u_offset,
59 v_offset,
60 bytes,
61 })
62 }
63
64 fn planes(self, size: RenderSize) -> [(u64, u32, u32, u32); 3] {
66 let (chroma_width, chroma_rows) = (size.width / 2, size.height / 2);
67 [
68 (0, self.luma_pitch, size.width, size.height),
69 (self.u_offset, self.chroma_pitch, chroma_width, chroma_rows),
70 (self.v_offset, self.chroma_pitch, chroma_width, chroma_rows),
71 ]
72 }
73}
74
75pub fn yuv420p_staging_bytes(size: RenderSize) -> Result<u64, RenderError> {
81 Yuv420StagingLayout::new(size).map(|layout| layout.bytes + CANARY_BYTES)
82}
83
84fn plane_rows(
85 mapped: &[u8],
86 (offset, pitch, width, rows): (u64, u32, u32, u32),
87) -> Result<std::iter::Take<std::slice::ChunksExact<'_, u8>>, RenderError> {
88 let offset = usize::try_from(offset)
89 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
90 let pitch = usize::try_from(pitch)
91 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
92 let width = usize::try_from(width)
93 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
94 let rows =
95 usize::try_from(rows).map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
96 let region = pitch
97 .checked_mul(rows)
98 .and_then(|length| length.checked_add(offset))
99 .and_then(|end| mapped.get(offset..end))
100 .filter(|_| width <= pitch)
101 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
102 Ok(region.chunks_exact(pitch).take(rows))
103}
104
105pub(crate) struct EncodedYuv420pReadback {
106 buffer: ReadbackBufferLease,
107 size: RenderSize,
108 layout: Yuv420StagingLayout,
109 staging_bytes: u64,
110 canary: Canary,
111 y_texture: TextureLease,
112 u_texture: TextureLease,
113 v_texture: TextureLease,
114 uniforms: UniformArenaLease,
115}
116
117impl EncodedYuv420pReadback {
118 pub(crate) fn flush_uniforms(&self, queue: &wgpu::Queue) {
119 self.uniforms.flush(queue);
120 }
121}
122
123impl Renderer {
124 pub fn upload_rgba8(
129 &self,
130 size: RenderSize,
131 pixels: &[u8],
132 ) -> Result<crate::UploadTexture, RenderError> {
133 self.write_rgba8(
134 size,
135 crate::SourcePlane {
136 bytes: pixels,
137 stride: size
138 .width
139 .checked_mul(RGBA8_BYTES_PER_PIXEL)
140 .ok_or(RenderError::InvalidImageData)?,
141 },
142 )
143 }
144
145 pub fn upload_linear_rgba16f(
151 &self,
152 size: RenderSize,
153 pixels: &[u16],
154 ) -> Result<crate::UploadTexture, RenderError> {
155 let bytes: std::borrow::Cow<'_, [u8]> = if cfg!(target_endian = "little") {
157 std::borrow::Cow::Borrowed(bytemuck::cast_slice(pixels))
158 } else {
159 std::borrow::Cow::Owned(pixels.iter().flat_map(|half| half.to_le_bytes()).collect())
160 };
161 self.write_linear_rgba16f(
162 size,
163 crate::SourcePlane {
164 bytes: &bytes,
165 stride: size
166 .width
167 .checked_mul(RGBA16F_BYTES_PER_PIXEL)
168 .ok_or(RenderError::InvalidImageData)?,
169 },
170 )
171 }
172
173 pub fn write_linear_rgba16f(
178 &self,
179 size: RenderSize,
180 plane: crate::SourcePlane<'_>,
181 ) -> Result<crate::UploadTexture, RenderError> {
182 self.validate_size(size)?;
183 let texture = self.upload_textures.acquire(
184 self.gpu.device(),
185 size,
186 wgpu::TextureFormat::Rgba16Float,
187 "alpha2 linear RGBA upload texture",
188 );
189 self.write_plane(&texture, size, plane, RGBA16F_BYTES_PER_PIXEL)?;
190 Ok(texture)
191 }
192
193 pub(crate) fn write_rgba8(
194 &self,
195 size: RenderSize,
196 plane: crate::SourcePlane<'_>,
197 ) -> Result<crate::UploadTexture, RenderError> {
198 let texture = self.acquire_rgba8_upload_texture(size)?;
199 self.write_plane(&texture, size, plane, RGBA8_BYTES_PER_PIXEL)?;
200 Ok(texture)
201 }
202
203 fn write_plane(
204 &self,
205 texture: &crate::UploadTexture,
206 size: RenderSize,
207 plane: crate::SourcePlane<'_>,
208 bytes_per_pixel: u32,
209 ) -> Result<(), RenderError> {
210 let row_bytes = size
211 .width
212 .checked_mul(bytes_per_pixel)
213 .ok_or(RenderError::InvalidImageData)?;
214 let required = usize::try_from(plane.stride)
215 .ok()
216 .zip(usize::try_from(size.height).ok())
217 .and_then(|(stride, rows)| stride.checked_mul(rows));
218 if plane.stride < row_bytes || required != Some(plane.bytes.len()) {
219 return Err(RenderError::InvalidImageData);
220 }
221 self.gpu.queue().write_texture(
222 wgpu::TexelCopyTextureInfo {
223 texture: texture.texture(),
224 mip_level: 0,
225 origin: wgpu::Origin3d::ZERO,
226 aspect: wgpu::TextureAspect::All,
227 },
228 plane.bytes,
229 wgpu::TexelCopyBufferLayout {
230 offset: 0,
231 bytes_per_row: Some(plane.stride),
232 rows_per_image: Some(size.height),
233 },
234 wgpu::Extent3d {
235 width: size.width,
236 height: size.height,
237 depth_or_array_layers: 1,
238 },
239 );
240 Ok(())
241 }
242
243 #[cfg(not(target_arch = "wasm32"))]
252 pub fn read_rgba8(&self, target: &RenderedTexture) -> Result<Vec<u8>, RenderError> {
253 self.begin_rgba8_readback(target)?.wait()
254 }
255
256 pub fn begin_rgba8_readback(
265 &self,
266 target: &RenderedTexture,
267 ) -> Result<Rgba8Readback, RenderError> {
268 self.ensure_gpu_runs_work()?;
269 self.validate_size(target.size)?;
270 if !matches!(
271 target.format,
272 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba16Float
273 ) {
274 return Err(RenderError::TextureReadback(
275 ReadbackFailure::UnsupportedTarget,
276 ));
277 }
278
279 let unpadded_bytes_per_row = target
280 .size
281 .width
282 .checked_mul(RGBA8_BYTES_PER_PIXEL)
283 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
284 let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
285 let padded_bytes_per_row = unpadded_bytes_per_row
286 .checked_add(alignment - 1)
287 .map(|value| value / alignment)
288 .and_then(|rows| rows.checked_mul(alignment))
289 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
290 let image_bytes = u64::from(padded_bytes_per_row)
291 .checked_mul(u64::from(target.size.height))
292 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
293 let buffer_size = image_bytes
295 .checked_add(CANARY_BYTES)
296 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
297 let mut encoder = self.command_encoder("alpha2 RGBA8 readback encoder");
298 let buffer =
299 self.readback_buffers
300 .acquire(self.gpu.device(), buffer_size, "alpha2 RGBA8 readback");
301 let packed_texture = self.encode_readback_pack(target, &mut encoder);
302 let copy_source = packed_texture
303 .as_ref()
304 .map_or(&target.texture, TextureLease::texture);
305 encoder.copy_texture_to_buffer(
306 wgpu::TexelCopyTextureInfo {
307 texture: copy_source,
308 mip_level: 0,
309 origin: wgpu::Origin3d::ZERO,
310 aspect: wgpu::TextureAspect::All,
311 },
312 wgpu::TexelCopyBufferInfo {
313 buffer: buffer.buffer(),
314 layout: wgpu::TexelCopyBufferLayout {
315 offset: 0,
316 bytes_per_row: Some(padded_bytes_per_row),
317 rows_per_image: Some(target.size.height),
318 },
319 },
320 wgpu::Extent3d {
321 width: target.size.width,
322 height: target.size.height,
323 depth_or_array_layers: 1,
324 },
325 );
326 let canary = self
328 .canaries
329 .encode(&self.gpu, &mut encoder, buffer.buffer(), image_bytes)?;
330 let command_buffer = encoder.finish();
331 #[cfg(not(target_arch = "wasm32"))]
332 let submission = self.submit([command_buffer])?;
333 #[cfg(target_arch = "wasm32")]
334 self.submit([command_buffer])?;
335 let render_passes = u64::from(packed_texture.is_some());
336 let copy_bytes =
337 u64::from(unpadded_bytes_per_row).saturating_mul(u64::from(target.size.height));
338 crate::diagnostics::record_readback_work(
339 &self.diagnostics,
340 render_passes,
341 1,
342 copy_bytes,
343 true,
344 );
345 Ok(Rgba8Readback {
346 buffer,
347 gpu: self.gpu.clone(),
348 size: target.size,
349 padded_bytes_per_row,
350 unpadded_bytes_per_row,
351 #[cfg(not(target_arch = "wasm32"))]
352 submission,
353 #[cfg(not(target_arch = "wasm32"))]
354 timeout: self.gpu_wait_timeout,
355 staging_bytes: buffer_size,
356 image_bytes,
357 canary,
358 mapping_requested: AtomicBool::new(false),
359 })
360 }
361
362 pub fn begin_yuv420p_readback(
371 &self,
372 target: &RenderedTexture,
373 encoding: YuvEncoding,
374 ) -> Result<Yuv420pReadback, RenderError> {
375 let mut command_encoder = self.command_encoder("alpha2 YUV420P readback encoder");
376 let readback_work = self.encode_yuv420p_readback(target, encoding, &mut command_encoder)?;
377 readback_work.flush_uniforms(self.gpu.queue());
378 #[cfg(not(target_arch = "wasm32"))]
379 let submission = self.submit([command_encoder.finish()])?;
380 #[cfg(target_arch = "wasm32")]
381 self.submit([command_encoder.finish()])?;
382 Ok(self.complete_yuv420p_readback(
383 readback_work,
384 #[cfg(not(target_arch = "wasm32"))]
385 submission,
386 true,
387 ))
388 }
389
390 pub fn begin_layout_readback(
399 &self,
400 target: &RenderedTexture,
401 encoding: YuvEncoding,
402 layout: PixelLayout,
403 ) -> Result<Yuv420pReadback, RenderError> {
404 self.ensure_gpu_runs_work()?;
405 self.validate_size(target.size)?;
406 let texel_bytes = match target.format {
407 wgpu::TextureFormat::Rgba16Float => 8,
408 wgpu::TextureFormat::Rgba8Unorm
409 if layout.bit_depth == alpha2_media::BitDepth::Eight =>
410 {
411 4
412 }
413 _ => {
414 return Err(RenderError::TextureReadback(
415 ReadbackFailure::UnsupportedTarget,
416 ));
417 }
418 };
419 let row_bytes = target
420 .size
421 .width
422 .checked_mul(texel_bytes)
423 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
424 let pitch = aligned_copy_row(row_bytes)?;
425 let image_bytes = u64::from(pitch)
426 .checked_mul(u64::from(target.size.height))
427 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
428 let buffer_size = image_bytes
429 .checked_add(CANARY_BYTES)
430 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
431 let mut encoder = self.command_encoder("alpha2 texel readback encoder");
432 let buffer =
433 self.readback_buffers
434 .acquire(self.gpu.device(), buffer_size, "alpha2 texel readback");
435 encoder.copy_texture_to_buffer(
436 wgpu::TexelCopyTextureInfo {
437 texture: &target.texture,
438 mip_level: 0,
439 origin: wgpu::Origin3d::ZERO,
440 aspect: wgpu::TextureAspect::All,
441 },
442 wgpu::TexelCopyBufferInfo {
443 buffer: buffer.buffer(),
444 layout: wgpu::TexelCopyBufferLayout {
445 offset: 0,
446 bytes_per_row: Some(pitch),
447 rows_per_image: Some(target.size.height),
448 },
449 },
450 wgpu::Extent3d {
451 width: target.size.width,
452 height: target.size.height,
453 depth_or_array_layers: 1,
454 },
455 );
456 let canary = self
457 .canaries
458 .encode(&self.gpu, &mut encoder, buffer.buffer(), image_bytes)?;
459 #[cfg(not(target_arch = "wasm32"))]
460 let submission = self.submit([encoder.finish()])?;
461 #[cfg(target_arch = "wasm32")]
462 self.submit([encoder.finish()])?;
463 crate::diagnostics::record_readback_work(
464 &self.diagnostics,
465 0,
466 1,
467 u64::from(row_bytes).saturating_mul(u64::from(target.size.height)),
468 true,
469 );
470 Ok(Yuv420pReadback {
471 buffer,
472 gpu: self.gpu.clone(),
473 size: target.size,
474 layout: Yuv420StagingLayout {
475 luma_pitch: 0,
476 chroma_pitch: 0,
477 u_offset: 0,
478 v_offset: 0,
479 bytes: 0,
480 },
481 texels: Some(Box::new(TexelStaging {
482 format: target.format,
483 pitch,
484 bytes: image_bytes,
485 layout,
486 encoding,
487 })),
488 #[cfg(not(target_arch = "wasm32"))]
489 submission,
490 #[cfg(not(target_arch = "wasm32"))]
491 timeout: self.gpu_wait_timeout,
492 staging_bytes: buffer_size,
493 canary,
494 mapping_requested: AtomicBool::new(false),
495 })
496 }
497
498 pub(crate) fn encode_yuv420p_readback(
499 &self,
500 target: &RenderedTexture,
501 encoding: YuvEncoding,
502 encoder: &mut wgpu::CommandEncoder,
503 ) -> Result<EncodedYuv420pReadback, RenderError> {
504 self.ensure_gpu_runs_work()?;
505 self.validate_size(target.size)?;
506 if !matches!(
507 target.format,
508 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba16Float
509 ) || !target.size.width.is_multiple_of(2)
510 || !target.size.height.is_multiple_of(2)
511 {
512 return Err(RenderError::TextureReadback(
513 ReadbackFailure::UnsupportedTarget,
514 ));
515 }
516 let layout = Yuv420StagingLayout::new(target.size)?;
517 let uv_rows = target.size.height / 2;
518 let chroma_width = target.size.width / 2;
519 let buffer_size = layout.bytes + CANARY_BYTES;
520 let buffer = self.readback_buffers.acquire(
521 self.gpu.device(),
522 buffer_size,
523 "alpha2 YUV420P readback",
524 );
525 let y_texture = self.textures.acquire(
526 self.gpu.device(),
527 target.size,
528 wgpu::TextureFormat::R8Unorm,
529 "alpha2 GPU Y readback texture",
530 );
531 let uv_size = RenderSize {
532 width: chroma_width,
533 height: uv_rows,
534 };
535 let u_texture = self.textures.acquire(
536 self.gpu.device(),
537 uv_size,
538 wgpu::TextureFormat::R8Unorm,
539 "alpha2 GPU U readback texture",
540 );
541 let v_texture = self.textures.acquire(
542 self.gpu.device(),
543 uv_size,
544 wgpu::TextureFormat::R8Unorm,
545 "alpha2 GPU V readback texture",
546 );
547 let uniform_bytes = f32_bytes(yuv_uniform(encoding));
548 let mut uniforms = self.uniform_buffers.acquire(
549 self.gpu.device(),
550 u64::try_from(uniform_bytes.len()).unwrap_or(u64::MAX),
551 );
552 let uniform = uniforms.write(&uniform_bytes);
553 self.encode_yuv420_pack(
554 target,
555 [y_texture.view(), u_texture.view(), v_texture.view()],
556 encoder,
557 &uniforms,
558 uniform,
559 );
560 encode_plane_copy(
561 encoder,
562 y_texture.texture(),
563 buffer.buffer(),
564 0,
565 layout.luma_pitch,
566 target.size,
567 );
568 encode_plane_copy(
569 encoder,
570 u_texture.texture(),
571 buffer.buffer(),
572 layout.u_offset,
573 layout.chroma_pitch,
574 uv_size,
575 );
576 encode_plane_copy(
577 encoder,
578 v_texture.texture(),
579 buffer.buffer(),
580 layout.v_offset,
581 layout.chroma_pitch,
582 uv_size,
583 );
584 let canary = self
586 .canaries
587 .encode(&self.gpu, encoder, buffer.buffer(), layout.bytes)?;
588 Ok(EncodedYuv420pReadback {
589 buffer,
590 size: target.size,
591 layout,
592 staging_bytes: buffer_size,
593 canary,
594 y_texture,
595 u_texture,
596 v_texture,
597 uniforms,
598 })
599 }
600
601 pub(crate) fn complete_yuv420p_readback(
602 &self,
603 encoded: EncodedYuv420pReadback,
604 #[cfg(not(target_arch = "wasm32"))] submission: wgpu::SubmissionIndex,
605 standalone_submission: bool,
606 ) -> Yuv420pReadback {
607 let EncodedYuv420pReadback {
608 buffer,
609 size,
610 layout,
611 staging_bytes,
612 canary,
613 y_texture,
614 u_texture,
615 v_texture,
616 uniforms,
617 } = encoded;
618 uniforms.recycle();
619 drop((y_texture, u_texture, v_texture));
620 let texture_bytes = yuv420_tight_bytes(size).expect("validated YUV420 dimensions");
621 crate::diagnostics::record_readback_work(
622 &self.diagnostics,
623 2,
624 3,
625 texture_bytes,
626 standalone_submission,
627 );
628 Yuv420pReadback {
629 buffer,
630 gpu: self.gpu.clone(),
631 size,
632 layout,
633 texels: None,
634 #[cfg(not(target_arch = "wasm32"))]
635 submission,
636 #[cfg(not(target_arch = "wasm32"))]
637 timeout: self.gpu_wait_timeout,
638 staging_bytes,
639 canary,
640 mapping_requested: AtomicBool::new(false),
641 }
642 }
643
644 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
655 pub fn encode_nv12_pack(
656 &self,
657 target: &RenderedTexture,
658 planes: Nv12PlaneTargets<'_>,
659 encoding: YuvEncoding,
660 encoder: &mut wgpu::CommandEncoder,
661 ) -> Result<EncodedNv12Pack, RenderError> {
662 self.ensure_gpu_runs_work()?;
664 let encode = self.open_encode();
665 self.validate_size(target.size)?;
666 if !matches!(
667 target.format,
668 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba16Float
669 ) || !target.size.width.is_multiple_of(2)
670 || !target.size.height.is_multiple_of(2)
671 {
672 return Err(RenderError::TextureReadback(
673 ReadbackFailure::UnsupportedTarget,
674 ));
675 }
676 let chroma_size = RenderSize {
677 width: target.size.width / 2,
678 height: target.size.height / 2,
679 };
680 let attachment = |texture: &wgpu::Texture| {
683 texture
684 .usage()
685 .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
686 && texture.mip_level_count() == 1
687 && texture.sample_count() == 1
688 && texture.depth_or_array_layers() == 1
689 };
690 if planes.luma.format() != wgpu::TextureFormat::R8Unorm
691 || planes.chroma.format() != wgpu::TextureFormat::Rg8Unorm
692 || planes.luma.width() != target.size.width
693 || planes.luma.height() != target.size.height
694 || planes.chroma.width() != chroma_size.width
695 || planes.chroma.height() != chroma_size.height
696 || !attachment(planes.luma)
697 || !attachment(planes.chroma)
698 {
699 return Err(RenderError::InvalidPlaneTarget);
700 }
701 let uniform_bytes = f32_bytes(yuv_uniform(encoding));
702 let mut uniforms = self.uniform_buffers.acquire(
703 self.gpu.device(),
704 u64::try_from(uniform_bytes.len()).unwrap_or(u64::MAX),
705 );
706 let uniform = uniforms.write(&uniform_bytes);
707 let cached = self.yuv_readback_pipeline();
708 let bind_group = self.yuv_pack_bind_group(target, cached, &uniforms, uniform);
709 let luma_view = crate::pool::default_view(planes.luma);
710 encode_yuv_render_pass(
711 encoder,
712 "alpha2 GPU NV12 Y pack pass",
713 &cached.y_pipeline,
714 &bind_group,
715 &luma_view,
716 );
717 let chroma_view = crate::pool::default_view(planes.chroma);
718 encode_yuv_render_pass(
719 encoder,
720 "alpha2 GPU NV12 UV pack pass",
721 &cached.uv_nv12_pipeline,
722 &bind_group,
723 &chroma_view,
724 );
725 uniforms.flush(self.gpu.queue());
727 Ok(EncodedNv12Pack {
730 uniforms: Some(uniforms),
731 _encode: encode,
732 })
733 }
734
735 fn yuv_pack_bind_group(
736 &self,
737 target: &RenderedTexture,
738 cached: &CachedYuvReadbackPipeline,
739 uniforms: &crate::uniform::UniformArenaLease,
740 uniform: crate::uniform::UniformSlice,
741 ) -> wgpu::BindGroup {
742 self.gpu
743 .device()
744 .create_bind_group(&wgpu::BindGroupDescriptor {
745 label: Some("alpha2 RGB to YUV bind group"),
746 layout: &cached.bind_group_layout,
747 entries: &[
748 wgpu::BindGroupEntry {
749 binding: 0,
750 resource: wgpu::BindingResource::TextureView(target.view()),
751 },
752 wgpu::BindGroupEntry {
753 binding: 1,
754 resource: uniforms.binding(uniform),
755 },
756 ],
757 })
758 }
759
760 fn encode_yuv420_pack(
761 &self,
762 target: &RenderedTexture,
763 plane_views: [&wgpu::TextureView; 3],
764 encoder: &mut wgpu::CommandEncoder,
765 uniforms: &crate::uniform::UniformArenaLease,
766 uniform: crate::uniform::UniformSlice,
767 ) {
768 let cached = self.yuv_readback_pipeline();
769 let bind_group = self.yuv_pack_bind_group(target, cached, uniforms, uniform);
770 let [y_view, u_view, v_view] = plane_views;
771 encode_yuv_render_pass(
772 encoder,
773 "alpha2 GPU Y pack pass",
774 &cached.y_pipeline,
775 &bind_group,
776 y_view,
777 );
778 encode_yuv_render_pass_pair(
779 encoder,
780 "alpha2 GPU UV pack pass",
781 &cached.uv_pipeline,
782 &bind_group,
783 [u_view, v_view],
784 );
785 }
786
787 fn yuv_readback_pipeline(&self) -> &CachedYuvReadbackPipeline {
788 self.pipelines.yuv_readback.get_or_init(|| {
789 let bind_group_layout =
790 self.gpu
791 .device()
792 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
793 label: Some("alpha2 RGB to YUV layout"),
794 entries: &[
795 wgpu::BindGroupLayoutEntry {
796 binding: 0,
797 visibility: wgpu::ShaderStages::FRAGMENT,
798 ty: wgpu::BindingType::Texture {
799 sample_type: wgpu::TextureSampleType::Float {
800 filterable: false,
801 },
802 view_dimension: wgpu::TextureViewDimension::D2,
803 multisampled: false,
804 },
805 count: None,
806 },
807 wgpu::BindGroupLayoutEntry {
808 binding: 1,
809 visibility: wgpu::ShaderStages::FRAGMENT,
810 ty: wgpu::BindingType::Buffer {
811 ty: wgpu::BufferBindingType::Uniform,
812 has_dynamic_offset: false,
813 min_binding_size: None,
814 },
815 count: None,
816 },
817 ],
818 });
819 let shader = self
820 .gpu
821 .device()
822 .create_shader_module(wgpu::ShaderModuleDescriptor {
823 label: Some("alpha2 RGB to YUV shader"),
824 source: wgpu::ShaderSource::Wgsl(RGB_TO_YUV420_WGSL.into()),
825 });
826 let layout =
827 self.gpu
828 .device()
829 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
830 label: Some("alpha2 RGB to YUV pipeline layout"),
831 bind_group_layouts: &[Some(&bind_group_layout)],
832 immediate_size: 0,
833 });
834 CachedYuvReadbackPipeline {
835 y_pipeline: yuv_render_pipeline(
836 self.gpu.device(),
837 &layout,
838 &shader,
839 "y_fs",
840 &[wgpu::TextureFormat::R8Unorm],
841 ),
842 uv_pipeline: yuv_render_pipeline(
843 self.gpu.device(),
844 &layout,
845 &shader,
846 "uv_fs",
847 &[wgpu::TextureFormat::R8Unorm, wgpu::TextureFormat::R8Unorm],
848 ),
849 uv_nv12_pipeline: yuv_render_pipeline(
850 self.gpu.device(),
851 &layout,
852 &shader,
853 "uv_nv12_fs",
854 &[wgpu::TextureFormat::Rg8Unorm],
855 ),
856 bind_group_layout,
857 }
858 })
859 }
860
861 fn encode_readback_pack(
862 &self,
863 target: &RenderedTexture,
864 encoder: &mut wgpu::CommandEncoder,
865 ) -> Option<TextureLease> {
866 if target.format != wgpu::TextureFormat::Rgba16Float {
867 return None;
868 }
869 let texture = self.textures.acquire(
870 self.gpu.device(),
871 target.size,
872 wgpu::TextureFormat::Rgba8Unorm,
873 "alpha2 GPU-packed RGBA8 readback texture",
874 );
875 let cached = self.readback_pack_pipeline();
876 let bind_group = self
877 .gpu
878 .device()
879 .create_bind_group(&wgpu::BindGroupDescriptor {
880 label: Some("alpha2 readback pack bind group"),
881 layout: &cached.bind_group_layout,
882 entries: &[wgpu::BindGroupEntry {
883 binding: 0,
884 resource: wgpu::BindingResource::TextureView(target.view()),
885 }],
886 });
887 {
888 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
889 label: Some("alpha2 GPU RGBA8 readback pack pass"),
890 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
891 view: texture.view(),
892 depth_slice: None,
893 resolve_target: None,
894 ops: wgpu::Operations {
895 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
896 store: wgpu::StoreOp::Store,
897 },
898 })],
899 depth_stencil_attachment: None,
900 timestamp_writes: None,
901 occlusion_query_set: None,
902 multiview_mask: None,
903 });
904 pass.set_pipeline(&cached.pipeline);
905 pass.set_bind_group(0, &bind_group, &[]);
906 pass.draw(0..3, 0..1);
907 }
908 Some(texture)
909 }
910
911 fn readback_pack_pipeline(&self) -> &CachedPipeline {
912 self.pipelines.readback_pack.get_or_init(|| {
913 let bind_group_layout =
914 self.gpu
915 .device()
916 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
917 label: Some("alpha2 readback pack layout"),
918 entries: &[wgpu::BindGroupLayoutEntry {
919 binding: 0,
920 visibility: wgpu::ShaderStages::FRAGMENT,
921 ty: wgpu::BindingType::Texture {
922 sample_type: wgpu::TextureSampleType::Float { filterable: false },
923 view_dimension: wgpu::TextureViewDimension::D2,
924 multisampled: false,
925 },
926 count: None,
927 }],
928 });
929 let shader = self
930 .gpu
931 .device()
932 .create_shader_module(wgpu::ShaderModuleDescriptor {
933 label: Some("alpha2 readback pack shader"),
934 source: wgpu::ShaderSource::Wgsl(READBACK_PACK_WGSL.into()),
935 });
936 let layout =
937 self.gpu
938 .device()
939 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
940 label: Some("alpha2 readback pack pipeline layout"),
941 bind_group_layouts: &[Some(&bind_group_layout)],
942 immediate_size: 0,
943 });
944 let pipeline =
945 self.gpu
946 .device()
947 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
948 label: Some("alpha2 readback pack pipeline"),
949 layout: Some(&layout),
950 vertex: wgpu::VertexState {
951 module: &shader,
952 entry_point: Some("vs"),
953 buffers: &[],
954 compilation_options: wgpu::PipelineCompilationOptions::default(),
955 },
956 fragment: Some(wgpu::FragmentState {
957 module: &shader,
958 entry_point: Some("fs"),
959 targets: &[Some(wgpu::TextureFormat::Rgba8Unorm.into())],
960 compilation_options: wgpu::PipelineCompilationOptions::default(),
961 }),
962 primitive: wgpu::PrimitiveState::default(),
963 depth_stencil: None,
964 multisample: wgpu::MultisampleState::default(),
965 multiview_mask: None,
966 cache: None,
967 });
968 CachedPipeline {
969 bind_group_layout,
970 pipeline,
971 }
972 })
973 }
974}
975
976#[derive(Debug)]
980pub struct Rgba8Readback {
981 buffer: ReadbackBufferLease,
982 gpu: GpuContext,
983 size: RenderSize,
984 padded_bytes_per_row: u32,
985 unpadded_bytes_per_row: u32,
986 #[cfg(not(target_arch = "wasm32"))]
987 submission: wgpu::SubmissionIndex,
988 #[cfg(not(target_arch = "wasm32"))]
989 timeout: std::time::Duration,
990 staging_bytes: u64,
991 image_bytes: u64,
992 canary: Canary,
993 mapping_requested: AtomicBool,
994}
995
996impl Rgba8Readback {
997 #[cfg(test)]
999 pub(crate) fn expecting(mut self, canary: Canary) -> Self {
1000 self.canary = canary;
1001 self
1002 }
1003
1004 #[must_use]
1006 pub const fn size(&self) -> RenderSize {
1007 self.size
1008 }
1009
1010 #[must_use]
1012 pub const fn staging_bytes(&self) -> u64 {
1013 self.staging_bytes
1014 }
1015
1016 #[cfg(not(target_arch = "wasm32"))]
1021 pub fn wait_ready(&self) -> Result<(), RenderError> {
1022 let (sender, receiver) = mpsc::channel();
1023 self.map_async(move |mapped| {
1024 let _ = sender.send(mapped);
1025 });
1026 self.gpu
1027 .device()
1028 .poll(wgpu::PollType::Wait {
1029 submission_index: Some(self.submission.clone()),
1030 timeout: Some(self.timeout),
1031 })
1032 .map_err(crate::error::poll_error)?;
1033 receiver
1034 .recv()
1035 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::CallbackDropped))?
1036 }
1037
1038 #[cfg(not(target_arch = "wasm32"))]
1043 pub fn wait(self) -> Result<Vec<u8>, RenderError> {
1044 self.wait_ready()?;
1045 self.finish()
1046 }
1047
1048 pub fn map_async(&self, on_ready: impl FnOnce(Result<(), RenderError>) + Send + 'static) {
1052 map_staging(&self.gpu, &self.buffer, &self.mapping_requested, on_ready);
1053 }
1054
1055 pub fn poll(&self) {
1057 let _ = self.gpu.device().poll(wgpu::PollType::Poll);
1058 }
1059
1060 pub fn finish(self) -> Result<Vec<u8>, RenderError> {
1067 let Ok(view) = self.buffer.buffer().slice(..).get_mapped_range() else {
1068 return Err(RenderError::TextureReadback(ReadbackFailure::NotMapped));
1071 };
1072 let image_bytes = usize::try_from(self.image_bytes)
1073 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1074 let checked = view
1075 .get(image_bytes..)
1076 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))
1077 .and_then(|found| self.canary.check(found));
1078 if let Err(error) = checked {
1079 drop(view);
1080 self.buffer.buffer().unmap();
1081 return Err(error);
1082 }
1083 let mapped = &view[..image_bytes];
1084 let padded_row = usize::try_from(self.padded_bytes_per_row)
1085 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1086 let unpadded_row = usize::try_from(self.unpadded_bytes_per_row)
1087 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1088 let packed = if padded_row == unpadded_row {
1089 mapped.to_vec()
1090 } else {
1091 let capacity = unpadded_row
1092 .checked_mul(
1093 usize::try_from(self.size.height)
1094 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?,
1095 )
1096 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
1097 let mut packed = Vec::with_capacity(capacity);
1098 for row in mapped.chunks_exact(padded_row) {
1099 packed.extend_from_slice(&row[..unpadded_row]);
1100 }
1101 packed
1102 };
1103 drop(view);
1104 self.buffer.buffer().unmap();
1105 self.buffer.recycle();
1106 Ok(packed)
1107 }
1108}
1109
1110#[must_use = "hold the pack until its encoder is submitted"]
1115#[derive(Debug)]
1116#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
1117pub struct EncodedNv12Pack {
1118 uniforms: Option<crate::uniform::UniformArenaLease>,
1119 _encode: crate::retire::EncodeScope,
1121}
1122
1123impl Drop for EncodedNv12Pack {
1124 fn drop(&mut self) {
1125 if let Some(uniforms) = self.uniforms.take() {
1126 uniforms.recycle();
1127 }
1128 }
1129}
1130
1131#[derive(Clone, Copy, Debug)]
1136#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
1137pub struct Nv12PlaneTargets<'a> {
1138 pub luma: &'a wgpu::Texture,
1140 pub chroma: &'a wgpu::Texture,
1142}
1143
1144#[derive(Debug)]
1146pub struct PlaneTarget<'a> {
1147 pub(crate) bytes: &'a mut [u8],
1148 pub(crate) stride: usize,
1149}
1150
1151impl<'a> PlaneTarget<'a> {
1152 #[must_use]
1154 pub fn new(bytes: &'a mut [u8], stride: usize) -> Self {
1155 Self { bytes, stride }
1156 }
1157
1158 #[must_use]
1160 pub fn bytes(&self) -> &[u8] {
1161 self.bytes
1162 }
1163
1164 pub fn reborrow(&mut self) -> PlaneTarget<'_> {
1166 PlaneTarget {
1167 bytes: self.bytes,
1168 stride: self.stride,
1169 }
1170 }
1171
1172 pub(crate) fn fits(&self, width: usize, height: usize) -> Result<(), RenderError> {
1173 if self.stride < width {
1174 return Err(RenderError::TextureReadback(
1175 ReadbackFailure::DestinationTooSmall,
1176 ));
1177 }
1178 let needed = self
1180 .stride
1181 .checked_mul(height.saturating_sub(1))
1182 .and_then(|rows| rows.checked_add(width))
1183 .ok_or(RenderError::TextureReadback(
1184 ReadbackFailure::DestinationTooSmall,
1185 ))?;
1186 if self.bytes.len() < needed {
1187 return Err(RenderError::TextureReadback(
1188 ReadbackFailure::DestinationTooSmall,
1189 ));
1190 }
1191 Ok(())
1192 }
1193
1194 fn row(&mut self, index: usize, width: usize) -> Result<&mut [u8], RenderError> {
1195 let start = index
1196 .checked_mul(self.stride)
1197 .ok_or(RenderError::TextureReadback(
1198 ReadbackFailure::DestinationTooSmall,
1199 ))?;
1200 let end = start
1201 .checked_add(width)
1202 .ok_or(RenderError::TextureReadback(
1203 ReadbackFailure::DestinationTooSmall,
1204 ))?;
1205 self.bytes
1206 .get_mut(start..end)
1207 .ok_or(RenderError::TextureReadback(
1208 ReadbackFailure::DestinationTooSmall,
1209 ))
1210 }
1211}
1212
1213#[derive(Debug)]
1215pub struct Yuv420pTargets<'a> {
1216 pub y: PlaneTarget<'a>,
1218 pub u: PlaneTarget<'a>,
1220 pub v: PlaneTarget<'a>,
1222}
1223
1224#[derive(Clone, Debug, Eq, PartialEq)]
1226pub struct Yuv420pPlanes {
1227 pub y: Vec<u8>,
1229 pub u: Vec<u8>,
1231 pub v: Vec<u8>,
1233}
1234
1235#[derive(Debug)]
1237pub struct Yuv420pReadback {
1238 buffer: ReadbackBufferLease,
1239 gpu: GpuContext,
1240 size: RenderSize,
1241 layout: Yuv420StagingLayout,
1242 texels: Option<Box<TexelStaging>>,
1245 #[cfg(not(target_arch = "wasm32"))]
1246 submission: wgpu::SubmissionIndex,
1247 #[cfg(not(target_arch = "wasm32"))]
1248 timeout: std::time::Duration,
1249 staging_bytes: u64,
1250 canary: Canary,
1251 mapping_requested: AtomicBool,
1252}
1253
1254impl Yuv420pReadback {
1255 #[must_use]
1257 pub const fn size(&self) -> RenderSize {
1258 self.size
1259 }
1260
1261 #[must_use]
1263 pub const fn staging_bytes(&self) -> u64 {
1264 self.staging_bytes
1265 }
1266
1267 #[cfg(not(target_arch = "wasm32"))]
1272 pub fn wait_ready(&self) -> Result<(), RenderError> {
1273 let (sender, receiver) = mpsc::channel();
1274 self.map_async(move |mapped| {
1275 let _ = sender.send(mapped);
1276 });
1277 self.wait_for_submission()?;
1278 receiver
1279 .recv()
1280 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::CallbackDropped))?
1281 }
1282
1283 #[cfg(not(target_arch = "wasm32"))]
1290 pub fn wait_for_submission(&self) -> Result<(), RenderError> {
1291 self.gpu
1292 .device()
1293 .poll(wgpu::PollType::Wait {
1294 submission_index: Some(self.submission.clone()),
1295 timeout: Some(self.timeout),
1296 })
1297 .map_err(crate::error::poll_error)?;
1298 Ok(())
1299 }
1300
1301 #[cfg(not(target_arch = "wasm32"))]
1306 pub fn wait(self) -> Result<Yuv420pPlanes, RenderError> {
1307 self.wait_ready()?;
1308 self.finish()
1309 }
1310
1311 pub fn map_async(&self, on_ready: impl FnOnce(Result<(), RenderError>) + Send + 'static) {
1313 map_staging(&self.gpu, &self.buffer, &self.mapping_requested, on_ready);
1314 }
1315
1316 pub fn poll(&self) {
1318 let _ = self.gpu.device().poll(wgpu::PollType::Poll);
1319 }
1320
1321 pub fn finish_into(self, targets: &mut Yuv420pTargets<'_>) -> Result<(), RenderError> {
1328 if self.texels.is_some() {
1329 return Err(RenderError::TextureReadback(ReadbackFailure::Layout));
1330 }
1331 let Ok(mapped) = self.buffer.buffer().slice(..).get_mapped_range() else {
1332 return Err(RenderError::TextureReadback(ReadbackFailure::NotMapped));
1334 };
1335 let result = (|| {
1336 self.check_canary(&mapped)?;
1337 let planes = self.layout.planes(self.size);
1338 let destinations = [&mut targets.y, &mut targets.u, &mut targets.v];
1339 for (plane, target) in planes.into_iter().zip(destinations) {
1340 let (_, _, width, rows) = plane;
1341 let width = usize::try_from(width)
1342 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1343 let rows = usize::try_from(rows)
1344 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1345 target.fits(width, rows)?;
1346 for (index, row) in plane_rows(&mapped, plane)?.enumerate() {
1347 target.row(index, width)?.copy_from_slice(&row[..width]);
1348 }
1349 }
1350 Ok(())
1351 })();
1352 drop(mapped);
1353 self.buffer.buffer().unmap();
1354 if result.is_ok() {
1355 self.buffer.recycle();
1356 }
1357 result
1358 }
1359
1360 pub fn finish(self) -> Result<Yuv420pPlanes, RenderError> {
1365 let planes = self.unpack(|[y, u, v]| Yuv420pPlanes { y, u, v })?;
1366 Ok(planes)
1367 }
1368
1369 #[must_use]
1371 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
1372 pub fn plane_layout(&self) -> [StagingPlane; 3] {
1373 self.layout
1374 .planes(self.size)
1375 .map(|(offset, stride, _, _)| StagingPlane { offset, stride })
1376 }
1377
1378 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
1387 pub fn finish_mapped<T>(
1388 self,
1389 read: impl FnOnce(&wgpu::BufferView) -> T,
1390 ) -> Result<T, RenderError> {
1391 let Ok(canary) = self
1392 .buffer
1393 .buffer()
1394 .slice(self.image_bytes()..)
1395 .get_mapped_range()
1396 else {
1397 return Err(RenderError::TextureReadback(ReadbackFailure::NotMapped));
1399 };
1400 let checked = self.canary.check(&canary);
1401 drop(canary);
1402 if let Err(error) = checked {
1403 self.buffer.buffer().unmap();
1404 return Err(error);
1405 }
1406 let Ok(mapped) = self
1407 .buffer
1408 .buffer()
1409 .slice(..self.image_bytes())
1410 .get_mapped_range()
1411 else {
1412 self.buffer.buffer().unmap();
1413 return Err(RenderError::TextureReadback(ReadbackFailure::NotMapped));
1414 };
1415 let result = read(&mapped);
1416 drop(mapped);
1417 self.buffer.buffer().unmap();
1418 self.buffer.recycle();
1419 Ok(result)
1420 }
1421
1422 fn unpack<T>(self, pack: impl FnOnce([Vec<u8>; 3]) -> T) -> Result<T, RenderError> {
1424 if self.texels.is_some() {
1425 return Err(RenderError::TextureReadback(ReadbackFailure::Layout));
1426 }
1427 let Ok(mapped) = self.buffer.buffer().slice(..).get_mapped_range() else {
1428 return Err(RenderError::TextureReadback(ReadbackFailure::NotMapped));
1430 };
1431 if let Err(error) = self.check_canary(&mapped) {
1432 drop(mapped);
1433 self.buffer.buffer().unmap();
1434 return Err(error);
1435 }
1436 let planes = self.layout.planes(self.size).map(|plane| {
1437 let (_, _, width, rows) = plane;
1438 let width = usize::try_from(width)
1439 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1440 let rows = usize::try_from(rows)
1441 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1442 plane_rows(&mapped, plane).map(|source| {
1443 let mut tight = Vec::with_capacity(width * rows);
1444 for row in source {
1445 tight.extend_from_slice(&row[..width]);
1446 }
1447 tight
1448 })
1449 });
1450 drop(mapped);
1451 self.buffer.buffer().unmap();
1452 let [y, u, v] = planes;
1453 let planes = [y?, u?, v?];
1454 self.buffer.recycle();
1455 Ok(pack(planes))
1456 }
1457
1458 fn image_bytes(&self) -> u64 {
1460 self.texels
1461 .as_ref()
1462 .map_or(self.layout.bytes, |texels| texels.bytes)
1463 }
1464
1465 #[must_use]
1467 pub fn pixel_layout(&self) -> Option<PixelLayout> {
1468 self.texels.as_ref().map(|texels| texels.layout)
1469 }
1470
1471 pub fn finish_into_planes(self, targets: &mut [PlaneTarget<'_>]) -> Result<(), RenderError> {
1477 let Some(texels) = self.texels.as_deref().cloned() else {
1478 let [y, u, v] = targets else {
1479 return Err(RenderError::TextureReadback(ReadbackFailure::Layout));
1480 };
1481 let mut planes = Yuv420pTargets {
1482 y: PlaneTarget::new(y.bytes, y.stride),
1483 u: PlaneTarget::new(u.bytes, u.stride),
1484 v: PlaneTarget::new(v.bytes, v.stride),
1485 };
1486 return self.finish_into(&mut planes);
1487 };
1488 let Ok(mapped) = self.buffer.buffer().slice(..).get_mapped_range() else {
1489 return Err(RenderError::TextureReadback(ReadbackFailure::NotMapped));
1491 };
1492 let result = (|| {
1493 self.check_canary(&mapped)?;
1494 let (width, height) = (self.size.width as usize, self.size.height as usize);
1495 let pitch = texels.pitch as usize;
1496 let rows = mapped
1497 .get(..pitch * height)
1498 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
1499 let row = |y: usize, out: &mut [[f32; 4]]| {
1500 let texels_row = &rows[y * pitch..];
1501 decode_texels(texels.format, texels_row, &mut out[..width]);
1502 };
1503 let picture = crate::layout_pack::Picture {
1504 width,
1505 height,
1506 row: &row,
1507 };
1508 crate::layout_pack::pack(&picture, texels.encoding, texels.layout, targets)
1509 })();
1510 drop(mapped);
1511 self.buffer.buffer().unmap();
1512 if result.is_ok() {
1513 self.buffer.recycle();
1514 }
1515 result
1516 }
1517
1518 fn check_canary(&self, mapped: &[u8]) -> Result<(), RenderError> {
1521 let start = usize::try_from(self.image_bytes())
1522 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1523 let found = mapped
1524 .get(start..)
1525 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
1526 self.canary.check(found)
1527 }
1528}
1529
1530fn map_staging(
1531 gpu: &GpuContext,
1532 buffer: &ReadbackBufferLease,
1533 requested: &AtomicBool,
1534 on_ready: impl FnOnce(Result<(), RenderError>) + Send + 'static,
1535) {
1536 if requested.swap(true, Ordering::AcqRel) {
1537 on_ready(Err(RenderError::TextureReadback(
1538 ReadbackFailure::DuplicateMapRequest,
1539 )));
1540 return;
1541 }
1542 gpu.map_async(
1543 &buffer.buffer().slice(..),
1544 wgpu::MapMode::Read,
1545 move |result| {
1546 on_ready(result.map_err(|_| RenderError::TextureReadback(ReadbackFailure::MapFailed)));
1548 },
1549 );
1550}
1551
1552#[derive(Clone, Debug)]
1553struct TexelStaging {
1554 format: wgpu::TextureFormat,
1555 pitch: u32,
1556 bytes: u64,
1557 layout: PixelLayout,
1558 encoding: YuvEncoding,
1559}
1560
1561fn decode_texels(format: wgpu::TextureFormat, row: &[u8], out: &mut [[f32; 4]]) {
1562 match format {
1563 wgpu::TextureFormat::Rgba16Float => {
1564 for (texel, bytes) in out.iter_mut().zip(row.as_chunks::<8>().0) {
1565 *texel = std::array::from_fn(|channel| {
1566 crate::cpu::half_value(u16::from_le_bytes([
1567 bytes[channel * 2],
1568 bytes[channel * 2 + 1],
1569 ]))
1570 });
1571 }
1572 }
1573 _ => {
1574 for (texel, bytes) in out.iter_mut().zip(row.as_chunks::<4>().0) {
1575 *texel = std::array::from_fn(|channel| crate::cpu::unorm8_value(bytes[channel]));
1576 }
1577 }
1578 }
1579}
1580
1581#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1583pub struct StagingPlane {
1584 pub offset: u64,
1586 pub stride: u32,
1588}
1589
1590fn aligned_copy_row(width: u32) -> Result<u32, RenderError> {
1591 let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
1592 width
1593 .checked_add(alignment - 1)
1594 .map(|value| value / alignment)
1595 .and_then(|rows| rows.checked_mul(alignment))
1596 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))
1597}
1598
1599fn yuv420_tight_bytes(size: RenderSize) -> Result<u64, RenderError> {
1600 u64::from(size.width)
1601 .checked_mul(u64::from(size.height))
1602 .and_then(|luma| luma.checked_mul(YUV420_PLANE_COUNT))
1603 .map(|three_planes| three_planes / 2)
1604 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))
1605}
1606
1607pub(crate) fn yuv_uniform(encoding: YuvEncoding) -> [f32; 12] {
1610 let YuvEncoding { color, chroma } = encoding;
1611 let (kr, kb) = color
1613 .matrix
1614 .luma_weights()
1615 .or(ColorMatrix::Bt709.luma_weights())
1616 .expect("BT.709 is a YCbCr matrix");
1617 let levels = color.range.levels(8);
1618 let (cosited_x, cosited_y) = match chroma {
1619 ChromaLocation::Left => (true, false),
1620 ChromaLocation::TopLeft => (true, true),
1621 ChromaLocation::Unspecified | ChromaLocation::Center => (false, false),
1622 };
1623 [
1624 kr,
1625 1.0 - kr - kb,
1626 kb,
1627 0.0,
1628 levels.luma_black / levels.maximum,
1629 levels.luma_span / levels.maximum,
1630 levels.chroma_center / levels.maximum,
1631 levels.chroma_span / levels.maximum,
1632 f32::from(u8::from(cosited_x)),
1633 f32::from(u8::from(cosited_y)),
1634 0.0,
1635 0.0,
1636 ]
1637}
1638
1639fn yuv_render_pipeline(
1640 device: &wgpu::Device,
1641 layout: &wgpu::PipelineLayout,
1642 shader: &wgpu::ShaderModule,
1643 fragment_entry: &'static str,
1644 target_formats: &[wgpu::TextureFormat],
1645) -> wgpu::RenderPipeline {
1646 let targets = target_formats
1647 .iter()
1648 .copied()
1649 .map(|format| Some(format.into()))
1650 .collect::<Vec<_>>();
1651 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1652 label: Some("alpha2 RGB to YUV pipeline"),
1653 layout: Some(layout),
1654 vertex: wgpu::VertexState {
1655 module: shader,
1656 entry_point: Some("vs"),
1657 buffers: &[],
1658 compilation_options: wgpu::PipelineCompilationOptions::default(),
1659 },
1660 fragment: Some(wgpu::FragmentState {
1661 module: shader,
1662 entry_point: Some(fragment_entry),
1663 targets: &targets,
1664 compilation_options: wgpu::PipelineCompilationOptions::default(),
1665 }),
1666 primitive: wgpu::PrimitiveState::default(),
1667 depth_stencil: None,
1668 multisample: wgpu::MultisampleState::default(),
1669 multiview_mask: None,
1670 cache: None,
1671 })
1672}
1673
1674fn encode_yuv_render_pass(
1675 encoder: &mut wgpu::CommandEncoder,
1676 label: &'static str,
1677 pipeline: &wgpu::RenderPipeline,
1678 bind_group: &wgpu::BindGroup,
1679 target: &wgpu::TextureView,
1680) {
1681 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1682 label: Some(label),
1683 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1684 view: target,
1685 depth_slice: None,
1686 resolve_target: None,
1687 ops: wgpu::Operations {
1688 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1689 store: wgpu::StoreOp::Store,
1690 },
1691 })],
1692 depth_stencil_attachment: None,
1693 timestamp_writes: None,
1694 occlusion_query_set: None,
1695 multiview_mask: None,
1696 });
1697 pass.set_pipeline(pipeline);
1698 pass.set_bind_group(0, bind_group, &[]);
1699 pass.draw(0..3, 0..1);
1700}
1701
1702fn encode_yuv_render_pass_pair(
1703 encoder: &mut wgpu::CommandEncoder,
1704 label: &'static str,
1705 pipeline: &wgpu::RenderPipeline,
1706 bind_group: &wgpu::BindGroup,
1707 targets: [&wgpu::TextureView; 2],
1708) {
1709 let attachments = targets.map(|target| {
1710 Some(wgpu::RenderPassColorAttachment {
1711 view: target,
1712 depth_slice: None,
1713 resolve_target: None,
1714 ops: wgpu::Operations {
1715 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1716 store: wgpu::StoreOp::Store,
1717 },
1718 })
1719 });
1720 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1721 label: Some(label),
1722 color_attachments: &attachments,
1723 depth_stencil_attachment: None,
1724 timestamp_writes: None,
1725 occlusion_query_set: None,
1726 multiview_mask: None,
1727 });
1728 pass.set_pipeline(pipeline);
1729 pass.set_bind_group(0, bind_group, &[]);
1730 pass.draw(0..3, 0..1);
1731}
1732
1733fn encode_plane_copy(
1734 encoder: &mut wgpu::CommandEncoder,
1735 texture: &wgpu::Texture,
1736 buffer: &wgpu::Buffer,
1737 offset: u64,
1738 bytes_per_row: u32,
1739 size: RenderSize,
1740) {
1741 encoder.copy_texture_to_buffer(
1742 wgpu::TexelCopyTextureInfo {
1743 texture,
1744 mip_level: 0,
1745 origin: wgpu::Origin3d::ZERO,
1746 aspect: wgpu::TextureAspect::All,
1747 },
1748 wgpu::TexelCopyBufferInfo {
1749 buffer,
1750 layout: wgpu::TexelCopyBufferLayout {
1751 offset,
1752 bytes_per_row: Some(bytes_per_row),
1753 rows_per_image: Some(size.height),
1754 },
1755 },
1756 wgpu::Extent3d {
1757 width: size.width,
1758 height: size.height,
1759 depth_or_array_layers: 1,
1760 },
1761 );
1762}
1763
1764#[cfg(test)]
1765pub(crate) fn read_rgba8(renderer: &Renderer, target: &RenderedTexture) -> Vec<u8> {
1766 renderer
1767 .read_rgba8(target)
1768 .expect("test texture readback succeeds")
1769}
1770
1771#[cfg(test)]
1772#[path = "readback_tests.rs"]
1773mod layout_tests;