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, 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;
24
25#[derive(Copy, Clone, Debug, Eq, PartialEq)]
31pub(crate) struct Yuv420StagingLayout {
32 luma_pitch: u32,
33 chroma_pitch: u32,
34 u_offset: u64,
35 v_offset: u64,
36 bytes: u64,
37}
38
39impl Yuv420StagingLayout {
40 pub(crate) fn new(size: RenderSize) -> Result<Self, RenderError> {
41 let luma_pitch = aligned_copy_row(size.width)?;
42 let chroma_pitch = aligned_copy_row(size.width / 2)?;
43 let region = |pitch: u32, rows: u32| u64::from(pitch).checked_mul(u64::from(rows));
44 let place = |end: u64| end.checked_next_multiple_of(PLANE_PLACEMENT_ALIGNMENT);
45 let chroma_bytes = region(chroma_pitch, size.height / 2)
46 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
47 let u_offset = region(luma_pitch, size.height)
48 .and_then(place)
49 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
50 let v_offset = u_offset
51 .checked_add(chroma_bytes)
52 .and_then(place)
53 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
54 let bytes = v_offset
55 .checked_add(chroma_bytes)
56 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
57 Ok(Self {
58 luma_pitch,
59 chroma_pitch,
60 u_offset,
61 v_offset,
62 bytes,
63 })
64 }
65
66 fn planes(self, size: RenderSize) -> [(u64, u32, u32, u32); 3] {
68 let (chroma_width, chroma_rows) = (size.width / 2, size.height / 2);
69 [
70 (0, self.luma_pitch, size.width, size.height),
71 (self.u_offset, self.chroma_pitch, chroma_width, chroma_rows),
72 (self.v_offset, self.chroma_pitch, chroma_width, chroma_rows),
73 ]
74 }
75}
76
77pub fn yuv420p_staging_bytes(size: RenderSize) -> Result<u64, RenderError> {
85 Yuv420StagingLayout::new(size).map(|layout| layout.bytes + CANARY_BYTES)
86}
87
88fn plane_rows(
90 mapped: &[u8],
91 (offset, pitch, width, rows): (u64, u32, u32, u32),
92) -> Result<std::iter::Take<std::slice::ChunksExact<'_, u8>>, RenderError> {
93 let offset = usize::try_from(offset)
94 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
95 let pitch = usize::try_from(pitch)
96 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
97 let width = usize::try_from(width)
98 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
99 let rows =
100 usize::try_from(rows).map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
101 let region = pitch
102 .checked_mul(rows)
103 .and_then(|length| length.checked_add(offset))
104 .and_then(|end| mapped.get(offset..end))
105 .filter(|_| width <= pitch)
106 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
107 Ok(region.chunks_exact(pitch).take(rows))
108}
109
110pub(crate) struct EncodedYuv420pReadback {
111 buffer: ReadbackBufferLease,
112 size: RenderSize,
113 layout: Yuv420StagingLayout,
114 staging_bytes: u64,
115 canary: Canary,
116 y_texture: TextureLease,
117 u_texture: TextureLease,
118 v_texture: TextureLease,
119 uniforms: UniformArenaLease,
120}
121
122impl EncodedYuv420pReadback {
123 pub(crate) fn flush_uniforms(&self, queue: &wgpu::Queue) {
124 self.uniforms.flush(queue);
125 }
126}
127
128impl Renderer {
129 pub fn upload_rgba8(
136 &self,
137 size: RenderSize,
138 pixels: &[u8],
139 ) -> Result<crate::UploadTexture, RenderError> {
140 self.write_rgba8(
141 size,
142 crate::SourcePlane {
143 bytes: pixels,
144 stride: size
145 .width
146 .checked_mul(RGBA8_BYTES_PER_PIXEL)
147 .ok_or(RenderError::InvalidImageData)?,
148 },
149 )
150 }
151
152 pub fn upload_linear_rgba16f(
160 &self,
161 size: RenderSize,
162 pixels: &[u16],
163 ) -> Result<crate::UploadTexture, RenderError> {
164 let bytes: std::borrow::Cow<'_, [u8]> = if cfg!(target_endian = "little") {
167 std::borrow::Cow::Borrowed(bytemuck::cast_slice(pixels))
168 } else {
169 std::borrow::Cow::Owned(pixels.iter().flat_map(|half| half.to_le_bytes()).collect())
170 };
171 self.write_linear_rgba16f(
172 size,
173 crate::SourcePlane {
174 bytes: &bytes,
175 stride: size
176 .width
177 .checked_mul(RGBA16F_BYTES_PER_PIXEL)
178 .ok_or(RenderError::InvalidImageData)?,
179 },
180 )
181 }
182
183 pub fn write_linear_rgba16f(
190 &self,
191 size: RenderSize,
192 plane: crate::SourcePlane<'_>,
193 ) -> Result<crate::UploadTexture, RenderError> {
194 self.validate_size(size)?;
195 let texture = self.upload_textures.acquire(
196 self.gpu.device(),
197 size,
198 wgpu::TextureFormat::Rgba16Float,
199 "alpha2 linear RGBA upload texture",
200 );
201 self.write_plane(&texture, size, plane, RGBA16F_BYTES_PER_PIXEL)?;
202 Ok(texture)
203 }
204
205 pub(crate) fn write_rgba8(
208 &self,
209 size: RenderSize,
210 plane: crate::SourcePlane<'_>,
211 ) -> Result<crate::UploadTexture, RenderError> {
212 let texture = self.acquire_rgba8_upload_texture(size)?;
213 self.write_plane(&texture, size, plane, RGBA8_BYTES_PER_PIXEL)?;
214 Ok(texture)
215 }
216
217 fn write_plane(
218 &self,
219 texture: &crate::UploadTexture,
220 size: RenderSize,
221 plane: crate::SourcePlane<'_>,
222 bytes_per_pixel: u32,
223 ) -> Result<(), RenderError> {
224 let row_bytes = size
225 .width
226 .checked_mul(bytes_per_pixel)
227 .ok_or(RenderError::InvalidImageData)?;
228 let required = usize::try_from(plane.stride)
229 .ok()
230 .zip(usize::try_from(size.height).ok())
231 .and_then(|(stride, rows)| stride.checked_mul(rows));
232 if plane.stride < row_bytes || required != Some(plane.bytes.len()) {
233 return Err(RenderError::InvalidImageData);
234 }
235 self.gpu.queue().write_texture(
236 wgpu::TexelCopyTextureInfo {
237 texture: texture.texture(),
238 mip_level: 0,
239 origin: wgpu::Origin3d::ZERO,
240 aspect: wgpu::TextureAspect::All,
241 },
242 plane.bytes,
243 wgpu::TexelCopyBufferLayout {
244 offset: 0,
245 bytes_per_row: Some(plane.stride),
246 rows_per_image: Some(size.height),
247 },
248 wgpu::Extent3d {
249 width: size.width,
250 height: size.height,
251 depth_or_array_layers: 1,
252 },
253 );
254 Ok(())
255 }
256
257 #[cfg(not(target_arch = "wasm32"))]
274 pub fn read_rgba8(&self, target: &RenderedTexture) -> Result<Vec<u8>, RenderError> {
275 self.begin_rgba8_readback(target)?.wait()
276 }
277
278 pub fn begin_rgba8_readback(
293 &self,
294 target: &RenderedTexture,
295 ) -> Result<Rgba8Readback, RenderError> {
296 self.ensure_gpu_runs_work()?;
297 self.validate_size(target.size)?;
298 if !matches!(
299 target.format,
300 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba16Float
301 ) {
302 return Err(RenderError::TextureReadback(
303 ReadbackFailure::UnsupportedTarget,
304 ));
305 }
306
307 let unpadded_bytes_per_row = target
308 .size
309 .width
310 .checked_mul(RGBA8_BYTES_PER_PIXEL)
311 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
312 let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
313 let padded_bytes_per_row = unpadded_bytes_per_row
314 .checked_add(alignment - 1)
315 .map(|value| value / alignment)
316 .and_then(|rows| rows.checked_mul(alignment))
317 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
318 let image_bytes = u64::from(padded_bytes_per_row)
319 .checked_mul(u64::from(target.size.height))
320 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
321 let buffer_size = image_bytes
323 .checked_add(CANARY_BYTES)
324 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
325 let mut encoder = self.command_encoder("alpha2 RGBA8 readback encoder");
326 let buffer =
327 self.readback_buffers
328 .acquire(self.gpu.device(), buffer_size, "alpha2 RGBA8 readback");
329 let packed_texture = self.encode_readback_pack(target, &mut encoder);
330 let copy_source = packed_texture
331 .as_ref()
332 .map_or(&target.texture, TextureLease::texture);
333 encoder.copy_texture_to_buffer(
334 wgpu::TexelCopyTextureInfo {
335 texture: copy_source,
336 mip_level: 0,
337 origin: wgpu::Origin3d::ZERO,
338 aspect: wgpu::TextureAspect::All,
339 },
340 wgpu::TexelCopyBufferInfo {
341 buffer: buffer.buffer(),
342 layout: wgpu::TexelCopyBufferLayout {
343 offset: 0,
344 bytes_per_row: Some(padded_bytes_per_row),
345 rows_per_image: Some(target.size.height),
346 },
347 },
348 wgpu::Extent3d {
349 width: target.size.width,
350 height: target.size.height,
351 depth_or_array_layers: 1,
352 },
353 );
354 let canary = self
356 .canaries
357 .encode(&self.gpu, &mut encoder, buffer.buffer(), image_bytes)?;
358 let command_buffer = encoder.finish();
359 #[cfg(not(target_arch = "wasm32"))]
360 let submission = self.submit([command_buffer])?;
361 #[cfg(target_arch = "wasm32")]
362 self.submit([command_buffer])?;
363 let render_passes = u64::from(packed_texture.is_some());
364 let copy_bytes =
365 u64::from(unpadded_bytes_per_row).saturating_mul(u64::from(target.size.height));
366 crate::diagnostics::record_readback_work(
367 &self.diagnostics,
368 render_passes,
369 1,
370 copy_bytes,
371 true,
372 );
373 Ok(Rgba8Readback {
374 buffer,
375 gpu: self.gpu.clone(),
376 size: target.size,
377 padded_bytes_per_row,
378 unpadded_bytes_per_row,
379 #[cfg(not(target_arch = "wasm32"))]
380 submission,
381 #[cfg(not(target_arch = "wasm32"))]
382 timeout: self.gpu_wait_timeout,
383 staging_bytes: buffer_size,
384 image_bytes,
385 canary,
386 mapping_requested: AtomicBool::new(false),
387 })
388 }
389
390 pub fn begin_yuv420p_readback(
402 &self,
403 target: &RenderedTexture,
404 encoding: YuvEncoding,
405 ) -> Result<Yuv420pReadback, RenderError> {
406 let mut command_encoder = self.command_encoder("alpha2 YUV420P readback encoder");
407 let readback_work = self.encode_yuv420p_readback(target, encoding, &mut command_encoder)?;
408 readback_work.flush_uniforms(self.gpu.queue());
409 #[cfg(not(target_arch = "wasm32"))]
410 let submission = self.submit([command_encoder.finish()])?;
411 #[cfg(target_arch = "wasm32")]
412 self.submit([command_encoder.finish()])?;
413 Ok(self.complete_yuv420p_readback(
414 readback_work,
415 #[cfg(not(target_arch = "wasm32"))]
416 submission,
417 true,
418 ))
419 }
420
421 pub(crate) fn encode_yuv420p_readback(
422 &self,
423 target: &RenderedTexture,
424 encoding: YuvEncoding,
425 encoder: &mut wgpu::CommandEncoder,
426 ) -> Result<EncodedYuv420pReadback, RenderError> {
427 self.ensure_gpu_runs_work()?;
428 self.validate_size(target.size)?;
429 if !matches!(
430 target.format,
431 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba16Float
432 ) || !target.size.width.is_multiple_of(2)
433 || !target.size.height.is_multiple_of(2)
434 {
435 return Err(RenderError::TextureReadback(
436 ReadbackFailure::UnsupportedTarget,
437 ));
438 }
439 let layout = Yuv420StagingLayout::new(target.size)?;
440 let uv_rows = target.size.height / 2;
441 let chroma_width = target.size.width / 2;
442 let buffer_size = layout.bytes + CANARY_BYTES;
443 let buffer = self.readback_buffers.acquire(
444 self.gpu.device(),
445 buffer_size,
446 "alpha2 YUV420P readback",
447 );
448 let y_texture = self.textures.acquire(
449 self.gpu.device(),
450 target.size,
451 wgpu::TextureFormat::R8Unorm,
452 "alpha2 GPU Y readback texture",
453 );
454 let uv_size = RenderSize {
455 width: chroma_width,
456 height: uv_rows,
457 };
458 let u_texture = self.textures.acquire(
459 self.gpu.device(),
460 uv_size,
461 wgpu::TextureFormat::R8Unorm,
462 "alpha2 GPU U readback texture",
463 );
464 let v_texture = self.textures.acquire(
465 self.gpu.device(),
466 uv_size,
467 wgpu::TextureFormat::R8Unorm,
468 "alpha2 GPU V readback texture",
469 );
470 let uniform_bytes = f32_bytes(yuv_uniform(encoding));
471 let mut uniforms = self.uniform_buffers.acquire(
472 self.gpu.device(),
473 u64::try_from(uniform_bytes.len()).unwrap_or(u64::MAX),
474 );
475 let uniform = uniforms.write(&uniform_bytes);
476 self.encode_yuv420_pack(
477 target,
478 [y_texture.view(), u_texture.view(), v_texture.view()],
479 encoder,
480 &uniforms,
481 uniform,
482 );
483 encode_plane_copy(
484 encoder,
485 y_texture.texture(),
486 buffer.buffer(),
487 0,
488 layout.luma_pitch,
489 target.size,
490 );
491 encode_plane_copy(
492 encoder,
493 u_texture.texture(),
494 buffer.buffer(),
495 layout.u_offset,
496 layout.chroma_pitch,
497 uv_size,
498 );
499 encode_plane_copy(
500 encoder,
501 v_texture.texture(),
502 buffer.buffer(),
503 layout.v_offset,
504 layout.chroma_pitch,
505 uv_size,
506 );
507 let canary = self
510 .canaries
511 .encode(&self.gpu, encoder, buffer.buffer(), layout.bytes)?;
512 Ok(EncodedYuv420pReadback {
513 buffer,
514 size: target.size,
515 layout,
516 staging_bytes: buffer_size,
517 canary,
518 y_texture,
519 u_texture,
520 v_texture,
521 uniforms,
522 })
523 }
524
525 pub(crate) fn complete_yuv420p_readback(
526 &self,
527 encoded: EncodedYuv420pReadback,
528 #[cfg(not(target_arch = "wasm32"))] submission: wgpu::SubmissionIndex,
529 standalone_submission: bool,
530 ) -> Yuv420pReadback {
531 let EncodedYuv420pReadback {
532 buffer,
533 size,
534 layout,
535 staging_bytes,
536 canary,
537 y_texture,
538 u_texture,
539 v_texture,
540 uniforms,
541 } = encoded;
542 uniforms.recycle();
543 drop((y_texture, u_texture, v_texture));
544 let texture_bytes = yuv420_tight_bytes(size).expect("validated YUV420 dimensions");
545 crate::diagnostics::record_readback_work(
546 &self.diagnostics,
547 2,
548 3,
549 texture_bytes,
550 standalone_submission,
551 );
552 Yuv420pReadback {
553 buffer,
554 gpu: self.gpu.clone(),
555 size,
556 layout,
557 #[cfg(not(target_arch = "wasm32"))]
558 submission,
559 #[cfg(not(target_arch = "wasm32"))]
560 timeout: self.gpu_wait_timeout,
561 staging_bytes,
562 canary,
563 mapping_requested: AtomicBool::new(false),
564 }
565 }
566
567 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
584 pub fn encode_nv12_pack(
585 &self,
586 target: &RenderedTexture,
587 planes: Nv12PlaneTargets<'_>,
588 encoding: YuvEncoding,
589 encoder: &mut wgpu::CommandEncoder,
590 ) -> Result<EncodedNv12Pack, RenderError> {
591 self.ensure_gpu_runs_work()?;
594 let encode = self.open_encode();
595 self.validate_size(target.size)?;
596 if !matches!(
597 target.format,
598 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba16Float
599 ) || !target.size.width.is_multiple_of(2)
600 || !target.size.height.is_multiple_of(2)
601 {
602 return Err(RenderError::TextureReadback(
603 ReadbackFailure::UnsupportedTarget,
604 ));
605 }
606 let chroma_size = RenderSize {
607 width: target.size.width / 2,
608 height: target.size.height / 2,
609 };
610 let attachment = |texture: &wgpu::Texture| {
613 texture
614 .usage()
615 .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
616 && texture.mip_level_count() == 1
617 && texture.sample_count() == 1
618 && texture.depth_or_array_layers() == 1
619 };
620 if planes.luma.format() != wgpu::TextureFormat::R8Unorm
621 || planes.chroma.format() != wgpu::TextureFormat::Rg8Unorm
622 || planes.luma.width() != target.size.width
623 || planes.luma.height() != target.size.height
624 || planes.chroma.width() != chroma_size.width
625 || planes.chroma.height() != chroma_size.height
626 || !attachment(planes.luma)
627 || !attachment(planes.chroma)
628 {
629 return Err(RenderError::InvalidPlaneTarget);
630 }
631 let uniform_bytes = f32_bytes(yuv_uniform(encoding));
632 let mut uniforms = self.uniform_buffers.acquire(
633 self.gpu.device(),
634 u64::try_from(uniform_bytes.len()).unwrap_or(u64::MAX),
635 );
636 let uniform = uniforms.write(&uniform_bytes);
637 let cached = self.yuv_readback_pipeline();
638 let bind_group = self.yuv_pack_bind_group(target, cached, &uniforms, uniform);
639 let luma_view = crate::pool::default_view(planes.luma);
640 encode_yuv_render_pass(
641 encoder,
642 "alpha2 GPU NV12 Y pack pass",
643 &cached.y_pipeline,
644 &bind_group,
645 &luma_view,
646 );
647 let chroma_view = crate::pool::default_view(planes.chroma);
648 encode_yuv_render_pass(
649 encoder,
650 "alpha2 GPU NV12 UV pack pass",
651 &cached.uv_nv12_pipeline,
652 &bind_group,
653 &chroma_view,
654 );
655 uniforms.flush(self.gpu.queue());
657 Ok(EncodedNv12Pack {
660 uniforms: Some(uniforms),
661 _encode: encode,
662 })
663 }
664
665 fn yuv_pack_bind_group(
666 &self,
667 target: &RenderedTexture,
668 cached: &CachedYuvReadbackPipeline,
669 uniforms: &crate::uniform::UniformArenaLease,
670 uniform: crate::uniform::UniformSlice,
671 ) -> wgpu::BindGroup {
672 self.gpu
673 .device()
674 .create_bind_group(&wgpu::BindGroupDescriptor {
675 label: Some("alpha2 RGB to YUV bind group"),
676 layout: &cached.bind_group_layout,
677 entries: &[
678 wgpu::BindGroupEntry {
679 binding: 0,
680 resource: wgpu::BindingResource::TextureView(target.view()),
681 },
682 wgpu::BindGroupEntry {
683 binding: 1,
684 resource: uniforms.binding(uniform),
685 },
686 ],
687 })
688 }
689
690 fn encode_yuv420_pack(
691 &self,
692 target: &RenderedTexture,
693 plane_views: [&wgpu::TextureView; 3],
694 encoder: &mut wgpu::CommandEncoder,
695 uniforms: &crate::uniform::UniformArenaLease,
696 uniform: crate::uniform::UniformSlice,
697 ) {
698 let cached = self.yuv_readback_pipeline();
699 let bind_group = self.yuv_pack_bind_group(target, cached, uniforms, uniform);
700 let [y_view, u_view, v_view] = plane_views;
701 encode_yuv_render_pass(
702 encoder,
703 "alpha2 GPU Y pack pass",
704 &cached.y_pipeline,
705 &bind_group,
706 y_view,
707 );
708 encode_yuv_render_pass_pair(
709 encoder,
710 "alpha2 GPU UV pack pass",
711 &cached.uv_pipeline,
712 &bind_group,
713 [u_view, v_view],
714 );
715 }
716
717 fn yuv_readback_pipeline(&self) -> &CachedYuvReadbackPipeline {
718 self.pipelines.yuv_readback.get_or_init(|| {
719 let bind_group_layout =
720 self.gpu
721 .device()
722 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
723 label: Some("alpha2 RGB to YUV layout"),
724 entries: &[
725 wgpu::BindGroupLayoutEntry {
726 binding: 0,
727 visibility: wgpu::ShaderStages::FRAGMENT,
728 ty: wgpu::BindingType::Texture {
729 sample_type: wgpu::TextureSampleType::Float {
730 filterable: false,
731 },
732 view_dimension: wgpu::TextureViewDimension::D2,
733 multisampled: false,
734 },
735 count: None,
736 },
737 wgpu::BindGroupLayoutEntry {
738 binding: 1,
739 visibility: wgpu::ShaderStages::FRAGMENT,
740 ty: wgpu::BindingType::Buffer {
741 ty: wgpu::BufferBindingType::Uniform,
742 has_dynamic_offset: false,
743 min_binding_size: None,
744 },
745 count: None,
746 },
747 ],
748 });
749 let shader = self
750 .gpu
751 .device()
752 .create_shader_module(wgpu::ShaderModuleDescriptor {
753 label: Some("alpha2 RGB to YUV shader"),
754 source: wgpu::ShaderSource::Wgsl(RGB_TO_YUV420_WGSL.into()),
755 });
756 let layout =
757 self.gpu
758 .device()
759 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
760 label: Some("alpha2 RGB to YUV pipeline layout"),
761 bind_group_layouts: &[Some(&bind_group_layout)],
762 immediate_size: 0,
763 });
764 CachedYuvReadbackPipeline {
765 y_pipeline: yuv_render_pipeline(
766 self.gpu.device(),
767 &layout,
768 &shader,
769 "y_fs",
770 &[wgpu::TextureFormat::R8Unorm],
771 ),
772 uv_pipeline: yuv_render_pipeline(
773 self.gpu.device(),
774 &layout,
775 &shader,
776 "uv_fs",
777 &[wgpu::TextureFormat::R8Unorm, wgpu::TextureFormat::R8Unorm],
778 ),
779 uv_nv12_pipeline: yuv_render_pipeline(
780 self.gpu.device(),
781 &layout,
782 &shader,
783 "uv_nv12_fs",
784 &[wgpu::TextureFormat::Rg8Unorm],
785 ),
786 bind_group_layout,
787 }
788 })
789 }
790
791 fn encode_readback_pack(
792 &self,
793 target: &RenderedTexture,
794 encoder: &mut wgpu::CommandEncoder,
795 ) -> Option<TextureLease> {
796 if target.format != wgpu::TextureFormat::Rgba16Float {
797 return None;
798 }
799 let texture = self.textures.acquire(
800 self.gpu.device(),
801 target.size,
802 wgpu::TextureFormat::Rgba8Unorm,
803 "alpha2 GPU-packed RGBA8 readback texture",
804 );
805 let cached = self.readback_pack_pipeline();
806 let bind_group = self
807 .gpu
808 .device()
809 .create_bind_group(&wgpu::BindGroupDescriptor {
810 label: Some("alpha2 readback pack bind group"),
811 layout: &cached.bind_group_layout,
812 entries: &[wgpu::BindGroupEntry {
813 binding: 0,
814 resource: wgpu::BindingResource::TextureView(target.view()),
815 }],
816 });
817 {
818 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
819 label: Some("alpha2 GPU RGBA8 readback pack pass"),
820 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
821 view: texture.view(),
822 depth_slice: None,
823 resolve_target: None,
824 ops: wgpu::Operations {
825 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
826 store: wgpu::StoreOp::Store,
827 },
828 })],
829 depth_stencil_attachment: None,
830 timestamp_writes: None,
831 occlusion_query_set: None,
832 multiview_mask: None,
833 });
834 pass.set_pipeline(&cached.pipeline);
835 pass.set_bind_group(0, &bind_group, &[]);
836 pass.draw(0..3, 0..1);
837 }
838 Some(texture)
839 }
840
841 fn readback_pack_pipeline(&self) -> &CachedPipeline {
842 self.pipelines.readback_pack.get_or_init(|| {
843 let bind_group_layout =
844 self.gpu
845 .device()
846 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
847 label: Some("alpha2 readback pack layout"),
848 entries: &[wgpu::BindGroupLayoutEntry {
849 binding: 0,
850 visibility: wgpu::ShaderStages::FRAGMENT,
851 ty: wgpu::BindingType::Texture {
852 sample_type: wgpu::TextureSampleType::Float { filterable: false },
853 view_dimension: wgpu::TextureViewDimension::D2,
854 multisampled: false,
855 },
856 count: None,
857 }],
858 });
859 let shader = self
860 .gpu
861 .device()
862 .create_shader_module(wgpu::ShaderModuleDescriptor {
863 label: Some("alpha2 readback pack shader"),
864 source: wgpu::ShaderSource::Wgsl(READBACK_PACK_WGSL.into()),
865 });
866 let layout =
867 self.gpu
868 .device()
869 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
870 label: Some("alpha2 readback pack pipeline layout"),
871 bind_group_layouts: &[Some(&bind_group_layout)],
872 immediate_size: 0,
873 });
874 let pipeline =
875 self.gpu
876 .device()
877 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
878 label: Some("alpha2 readback pack pipeline"),
879 layout: Some(&layout),
880 vertex: wgpu::VertexState {
881 module: &shader,
882 entry_point: Some("vs"),
883 buffers: &[],
884 compilation_options: wgpu::PipelineCompilationOptions::default(),
885 },
886 fragment: Some(wgpu::FragmentState {
887 module: &shader,
888 entry_point: Some("fs"),
889 targets: &[Some(wgpu::TextureFormat::Rgba8Unorm.into())],
890 compilation_options: wgpu::PipelineCompilationOptions::default(),
891 }),
892 primitive: wgpu::PrimitiveState::default(),
893 depth_stencil: None,
894 multisample: wgpu::MultisampleState::default(),
895 multiview_mask: None,
896 cache: None,
897 });
898 CachedPipeline {
899 bind_group_layout,
900 pipeline,
901 }
902 })
903 }
904}
905
906#[derive(Debug)]
912pub struct Rgba8Readback {
913 buffer: ReadbackBufferLease,
914 gpu: GpuContext,
915 size: RenderSize,
916 padded_bytes_per_row: u32,
917 unpadded_bytes_per_row: u32,
918 #[cfg(not(target_arch = "wasm32"))]
919 submission: wgpu::SubmissionIndex,
920 #[cfg(not(target_arch = "wasm32"))]
921 timeout: std::time::Duration,
922 staging_bytes: u64,
923 image_bytes: u64,
925 canary: Canary,
926 mapping_requested: AtomicBool,
927}
928
929impl Rgba8Readback {
930 #[cfg(test)]
933 pub(crate) fn expecting(mut self, canary: Canary) -> Self {
934 self.canary = canary;
935 self
936 }
937
938 #[must_use]
940 pub const fn size(&self) -> RenderSize {
941 self.size
942 }
943
944 #[must_use]
946 pub const fn staging_bytes(&self) -> u64 {
947 self.staging_bytes
948 }
949
950 #[cfg(not(target_arch = "wasm32"))]
960 pub fn wait_ready(&self) -> Result<(), RenderError> {
961 let (sender, receiver) = mpsc::channel();
962 self.map_async(move |mapped| {
963 let _ = sender.send(mapped);
964 });
965 self.gpu
966 .device()
967 .poll(wgpu::PollType::Wait {
968 submission_index: Some(self.submission.clone()),
969 timeout: Some(self.timeout),
970 })
971 .map_err(crate::error::poll_error)?;
972 receiver
973 .recv()
974 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::CallbackDropped))?
975 }
976
977 #[cfg(not(target_arch = "wasm32"))]
983 pub fn wait(self) -> Result<Vec<u8>, RenderError> {
984 self.wait_ready()?;
985 self.finish()
986 }
987
988 pub fn map_async(&self, on_ready: impl FnOnce(Result<(), RenderError>) + Send + 'static) {
994 map_staging(&self.gpu, &self.buffer, &self.mapping_requested, on_ready);
995 }
996
997 pub fn poll(&self) {
1002 let _ = self.gpu.device().poll(wgpu::PollType::Poll);
1003 }
1004
1005 pub fn finish(self) -> Result<Vec<u8>, RenderError> {
1013 let Ok(view) = self.buffer.buffer().slice(..).get_mapped_range() else {
1014 return Err(RenderError::TextureReadback(ReadbackFailure::NotMapped));
1017 };
1018 let image_bytes = usize::try_from(self.image_bytes)
1019 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1020 let checked = view
1021 .get(image_bytes..)
1022 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))
1023 .and_then(|found| self.canary.check(found));
1024 if let Err(error) = checked {
1025 drop(view);
1026 self.buffer.buffer().unmap();
1027 return Err(error);
1028 }
1029 let mapped = &view[..image_bytes];
1030 let padded_row = usize::try_from(self.padded_bytes_per_row)
1031 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1032 let unpadded_row = usize::try_from(self.unpadded_bytes_per_row)
1033 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1034 let packed = if padded_row == unpadded_row {
1035 mapped.to_vec()
1036 } else {
1037 let capacity = unpadded_row
1038 .checked_mul(
1039 usize::try_from(self.size.height)
1040 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?,
1041 )
1042 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
1043 let mut packed = Vec::with_capacity(capacity);
1044 for row in mapped.chunks_exact(padded_row) {
1045 packed.extend_from_slice(&row[..unpadded_row]);
1046 }
1047 packed
1048 };
1049 drop(view);
1050 self.buffer.buffer().unmap();
1051 self.buffer.recycle();
1052 Ok(packed)
1053 }
1054}
1055
1056#[must_use = "hold the pack until its encoder is submitted"]
1063#[derive(Debug)]
1064#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
1065pub struct EncodedNv12Pack {
1066 uniforms: Option<crate::uniform::UniformArenaLease>,
1067 _encode: crate::retire::EncodeScope,
1070}
1071
1072impl Drop for EncodedNv12Pack {
1073 fn drop(&mut self) {
1074 if let Some(uniforms) = self.uniforms.take() {
1075 uniforms.recycle();
1076 }
1077 }
1078}
1079
1080#[derive(Clone, Copy, Debug)]
1087#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
1088pub struct Nv12PlaneTargets<'a> {
1089 pub luma: &'a wgpu::Texture,
1091 pub chroma: &'a wgpu::Texture,
1093}
1094
1095#[derive(Debug)]
1097pub struct PlaneTarget<'a> {
1098 bytes: &'a mut [u8],
1099 stride: usize,
1100}
1101
1102impl<'a> PlaneTarget<'a> {
1103 #[must_use]
1105 pub fn new(bytes: &'a mut [u8], stride: usize) -> Self {
1106 Self { bytes, stride }
1107 }
1108
1109 #[must_use]
1111 pub fn bytes(&self) -> &[u8] {
1112 self.bytes
1113 }
1114
1115 fn fits(&self, width: usize, height: usize) -> Result<(), RenderError> {
1116 if self.stride < width {
1117 return Err(RenderError::TextureReadback(
1118 ReadbackFailure::DestinationTooSmall,
1119 ));
1120 }
1121 let needed = self
1124 .stride
1125 .checked_mul(height.saturating_sub(1))
1126 .and_then(|rows| rows.checked_add(width))
1127 .ok_or(RenderError::TextureReadback(
1128 ReadbackFailure::DestinationTooSmall,
1129 ))?;
1130 if self.bytes.len() < needed {
1131 return Err(RenderError::TextureReadback(
1132 ReadbackFailure::DestinationTooSmall,
1133 ));
1134 }
1135 Ok(())
1136 }
1137
1138 fn row(&mut self, index: usize, width: usize) -> Result<&mut [u8], RenderError> {
1139 let start = index
1140 .checked_mul(self.stride)
1141 .ok_or(RenderError::TextureReadback(
1142 ReadbackFailure::DestinationTooSmall,
1143 ))?;
1144 let end = start
1145 .checked_add(width)
1146 .ok_or(RenderError::TextureReadback(
1147 ReadbackFailure::DestinationTooSmall,
1148 ))?;
1149 self.bytes
1150 .get_mut(start..end)
1151 .ok_or(RenderError::TextureReadback(
1152 ReadbackFailure::DestinationTooSmall,
1153 ))
1154 }
1155}
1156
1157#[derive(Debug)]
1159pub struct Yuv420pTargets<'a> {
1160 pub y: PlaneTarget<'a>,
1162 pub u: PlaneTarget<'a>,
1164 pub v: PlaneTarget<'a>,
1166}
1167
1168#[derive(Clone, Debug, Eq, PartialEq)]
1170pub struct Yuv420pPlanes {
1171 pub y: Vec<u8>,
1173 pub u: Vec<u8>,
1175 pub v: Vec<u8>,
1177}
1178
1179#[derive(Debug)]
1181pub struct Yuv420pReadback {
1182 buffer: ReadbackBufferLease,
1183 gpu: GpuContext,
1184 size: RenderSize,
1185 layout: Yuv420StagingLayout,
1186 #[cfg(not(target_arch = "wasm32"))]
1187 submission: wgpu::SubmissionIndex,
1188 #[cfg(not(target_arch = "wasm32"))]
1189 timeout: std::time::Duration,
1190 staging_bytes: u64,
1191 canary: Canary,
1192 mapping_requested: AtomicBool,
1193}
1194
1195impl Yuv420pReadback {
1196 #[must_use]
1198 pub const fn size(&self) -> RenderSize {
1199 self.size
1200 }
1201
1202 #[must_use]
1204 pub const fn staging_bytes(&self) -> u64 {
1205 self.staging_bytes
1206 }
1207
1208 #[cfg(not(target_arch = "wasm32"))]
1213 pub fn wait_ready(&self) -> Result<(), RenderError> {
1214 let (sender, receiver) = mpsc::channel();
1215 self.map_async(move |mapped| {
1216 let _ = sender.send(mapped);
1217 });
1218 self.wait_for_submission()?;
1219 receiver
1220 .recv()
1221 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::CallbackDropped))?
1222 }
1223
1224 #[cfg(not(target_arch = "wasm32"))]
1234 pub fn wait_for_submission(&self) -> Result<(), RenderError> {
1235 self.gpu
1236 .device()
1237 .poll(wgpu::PollType::Wait {
1238 submission_index: Some(self.submission.clone()),
1239 timeout: Some(self.timeout),
1240 })
1241 .map_err(crate::error::poll_error)?;
1242 Ok(())
1243 }
1244
1245 #[cfg(not(target_arch = "wasm32"))]
1251 pub fn wait(self) -> Result<Yuv420pPlanes, RenderError> {
1252 self.wait_ready()?;
1253 self.finish()
1254 }
1255
1256 pub fn map_async(&self, on_ready: impl FnOnce(Result<(), RenderError>) + Send + 'static) {
1258 map_staging(&self.gpu, &self.buffer, &self.mapping_requested, on_ready);
1259 }
1260
1261 pub fn poll(&self) {
1263 let _ = self.gpu.device().poll(wgpu::PollType::Poll);
1264 }
1265
1266 pub fn finish_into(self, targets: &mut Yuv420pTargets<'_>) -> Result<(), RenderError> {
1283 let Ok(mapped) = self.buffer.buffer().slice(..).get_mapped_range() else {
1284 return Err(RenderError::TextureReadback(ReadbackFailure::NotMapped));
1286 };
1287 let result = (|| {
1288 self.check_canary(&mapped)?;
1289 let planes = self.layout.planes(self.size);
1290 let destinations = [&mut targets.y, &mut targets.u, &mut targets.v];
1291 for (plane, target) in planes.into_iter().zip(destinations) {
1292 let (_, _, width, rows) = plane;
1293 let width = usize::try_from(width)
1294 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1295 let rows = usize::try_from(rows)
1296 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1297 target.fits(width, rows)?;
1298 for (index, row) in plane_rows(&mapped, plane)?.enumerate() {
1299 target.row(index, width)?.copy_from_slice(&row[..width]);
1300 }
1301 }
1302 Ok(())
1303 })();
1304 drop(mapped);
1305 self.buffer.buffer().unmap();
1306 if result.is_ok() {
1307 self.buffer.recycle();
1308 }
1309 result
1310 }
1311
1312 pub fn finish(self) -> Result<Yuv420pPlanes, RenderError> {
1319 let planes = self.unpack(|[y, u, v]| Yuv420pPlanes { y, u, v })?;
1320 Ok(planes)
1321 }
1322
1323 #[must_use]
1326 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
1327 pub fn plane_layout(&self) -> [StagingPlane; 3] {
1328 self.layout
1329 .planes(self.size)
1330 .map(|(offset, stride, _, _)| StagingPlane { offset, stride })
1331 }
1332
1333 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
1348 pub fn finish_mapped<T>(
1349 self,
1350 read: impl FnOnce(&wgpu::BufferView) -> T,
1351 ) -> Result<T, RenderError> {
1352 let Ok(canary) = self
1353 .buffer
1354 .buffer()
1355 .slice(self.layout.bytes..)
1356 .get_mapped_range()
1357 else {
1358 return Err(RenderError::TextureReadback(ReadbackFailure::NotMapped));
1360 };
1361 let checked = self.canary.check(&canary);
1362 drop(canary);
1363 if let Err(error) = checked {
1364 self.buffer.buffer().unmap();
1365 return Err(error);
1366 }
1367 let Ok(mapped) = self
1368 .buffer
1369 .buffer()
1370 .slice(..self.layout.bytes)
1371 .get_mapped_range()
1372 else {
1373 self.buffer.buffer().unmap();
1374 return Err(RenderError::TextureReadback(ReadbackFailure::NotMapped));
1375 };
1376 let result = read(&mapped);
1377 drop(mapped);
1378 self.buffer.buffer().unmap();
1379 self.buffer.recycle();
1380 Ok(result)
1381 }
1382
1383 fn unpack<T>(self, pack: impl FnOnce([Vec<u8>; 3]) -> T) -> Result<T, RenderError> {
1386 let Ok(mapped) = self.buffer.buffer().slice(..).get_mapped_range() else {
1387 return Err(RenderError::TextureReadback(ReadbackFailure::NotMapped));
1389 };
1390 if let Err(error) = self.check_canary(&mapped) {
1391 drop(mapped);
1392 self.buffer.buffer().unmap();
1393 return Err(error);
1394 }
1395 let planes = self.layout.planes(self.size).map(|plane| {
1396 let (_, _, width, rows) = plane;
1397 let width = usize::try_from(width)
1398 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1399 let rows = usize::try_from(rows)
1400 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1401 plane_rows(&mapped, plane).map(|source| {
1402 let mut tight = Vec::with_capacity(width * rows);
1404 for row in source {
1405 tight.extend_from_slice(&row[..width]);
1406 }
1407 tight
1408 })
1409 });
1410 drop(mapped);
1411 self.buffer.buffer().unmap();
1412 let [y, u, v] = planes;
1413 let planes = [y?, u?, v?];
1414 self.buffer.recycle();
1415 Ok(pack(planes))
1416 }
1417
1418 fn check_canary(&self, mapped: &[u8]) -> Result<(), RenderError> {
1422 let start = usize::try_from(self.layout.bytes)
1423 .map_err(|_| RenderError::TextureReadback(ReadbackFailure::Layout))?;
1424 let found = mapped
1425 .get(start..)
1426 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))?;
1427 self.canary.check(found)
1428 }
1429}
1430
1431fn map_staging(
1433 gpu: &GpuContext,
1434 buffer: &ReadbackBufferLease,
1435 requested: &AtomicBool,
1436 on_ready: impl FnOnce(Result<(), RenderError>) + Send + 'static,
1437) {
1438 if requested.swap(true, Ordering::AcqRel) {
1439 on_ready(Err(RenderError::TextureReadback(
1440 ReadbackFailure::DuplicateMapRequest,
1441 )));
1442 return;
1443 }
1444 gpu.map_async(
1445 &buffer.buffer().slice(..),
1446 wgpu::MapMode::Read,
1447 move |result| {
1448 on_ready(result.map_err(|_| RenderError::TextureReadback(ReadbackFailure::MapFailed)));
1451 },
1452 );
1453}
1454
1455#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1457pub struct StagingPlane {
1458 pub offset: u64,
1460 pub stride: u32,
1462}
1463
1464fn aligned_copy_row(width: u32) -> Result<u32, RenderError> {
1465 let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
1466 width
1467 .checked_add(alignment - 1)
1468 .map(|value| value / alignment)
1469 .and_then(|rows| rows.checked_mul(alignment))
1470 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))
1471}
1472
1473fn yuv420_tight_bytes(size: RenderSize) -> Result<u64, RenderError> {
1474 u64::from(size.width)
1475 .checked_mul(u64::from(size.height))
1476 .and_then(|luma| luma.checked_mul(YUV420_PLANE_COUNT))
1477 .map(|three_planes| three_planes / 2)
1478 .ok_or(RenderError::TextureReadback(ReadbackFailure::Layout))
1479}
1480
1481fn yuv_uniform(encoding: YuvEncoding) -> [f32; 12] {
1486 let YuvEncoding { color, chroma } = encoding;
1487 let (kr, kb) = color
1489 .matrix
1490 .luma_weights()
1491 .or(ColorMatrix::Bt709.luma_weights())
1492 .expect("BT.709 is a YCbCr matrix");
1493 let levels = color.range.levels(8);
1494 let (cosited_x, cosited_y) = match chroma {
1495 ChromaLocation::Left => (true, false),
1496 ChromaLocation::TopLeft => (true, true),
1497 ChromaLocation::Unspecified | ChromaLocation::Center => (false, false),
1498 };
1499 [
1500 kr,
1501 1.0 - kr - kb,
1502 kb,
1503 0.0,
1504 levels.luma_black / levels.maximum,
1505 levels.luma_span / levels.maximum,
1506 levels.chroma_center / levels.maximum,
1507 levels.chroma_span / levels.maximum,
1508 f32::from(u8::from(cosited_x)),
1509 f32::from(u8::from(cosited_y)),
1510 0.0,
1511 0.0,
1512 ]
1513}
1514
1515fn yuv_render_pipeline(
1516 device: &wgpu::Device,
1517 layout: &wgpu::PipelineLayout,
1518 shader: &wgpu::ShaderModule,
1519 fragment_entry: &'static str,
1520 target_formats: &[wgpu::TextureFormat],
1521) -> wgpu::RenderPipeline {
1522 let targets = target_formats
1523 .iter()
1524 .copied()
1525 .map(|format| Some(format.into()))
1526 .collect::<Vec<_>>();
1527 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1528 label: Some("alpha2 RGB to YUV pipeline"),
1529 layout: Some(layout),
1530 vertex: wgpu::VertexState {
1531 module: shader,
1532 entry_point: Some("vs"),
1533 buffers: &[],
1534 compilation_options: wgpu::PipelineCompilationOptions::default(),
1535 },
1536 fragment: Some(wgpu::FragmentState {
1537 module: shader,
1538 entry_point: Some(fragment_entry),
1539 targets: &targets,
1540 compilation_options: wgpu::PipelineCompilationOptions::default(),
1541 }),
1542 primitive: wgpu::PrimitiveState::default(),
1543 depth_stencil: None,
1544 multisample: wgpu::MultisampleState::default(),
1545 multiview_mask: None,
1546 cache: None,
1547 })
1548}
1549
1550fn encode_yuv_render_pass(
1551 encoder: &mut wgpu::CommandEncoder,
1552 label: &'static str,
1553 pipeline: &wgpu::RenderPipeline,
1554 bind_group: &wgpu::BindGroup,
1555 target: &wgpu::TextureView,
1556) {
1557 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1558 label: Some(label),
1559 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1560 view: target,
1561 depth_slice: None,
1562 resolve_target: None,
1563 ops: wgpu::Operations {
1564 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1565 store: wgpu::StoreOp::Store,
1566 },
1567 })],
1568 depth_stencil_attachment: None,
1569 timestamp_writes: None,
1570 occlusion_query_set: None,
1571 multiview_mask: None,
1572 });
1573 pass.set_pipeline(pipeline);
1574 pass.set_bind_group(0, bind_group, &[]);
1575 pass.draw(0..3, 0..1);
1576}
1577
1578fn encode_yuv_render_pass_pair(
1579 encoder: &mut wgpu::CommandEncoder,
1580 label: &'static str,
1581 pipeline: &wgpu::RenderPipeline,
1582 bind_group: &wgpu::BindGroup,
1583 targets: [&wgpu::TextureView; 2],
1584) {
1585 let attachments = targets.map(|target| {
1586 Some(wgpu::RenderPassColorAttachment {
1587 view: target,
1588 depth_slice: None,
1589 resolve_target: None,
1590 ops: wgpu::Operations {
1591 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1592 store: wgpu::StoreOp::Store,
1593 },
1594 })
1595 });
1596 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1597 label: Some(label),
1598 color_attachments: &attachments,
1599 depth_stencil_attachment: None,
1600 timestamp_writes: None,
1601 occlusion_query_set: None,
1602 multiview_mask: None,
1603 });
1604 pass.set_pipeline(pipeline);
1605 pass.set_bind_group(0, bind_group, &[]);
1606 pass.draw(0..3, 0..1);
1607}
1608
1609fn encode_plane_copy(
1610 encoder: &mut wgpu::CommandEncoder,
1611 texture: &wgpu::Texture,
1612 buffer: &wgpu::Buffer,
1613 offset: u64,
1614 bytes_per_row: u32,
1615 size: RenderSize,
1616) {
1617 encoder.copy_texture_to_buffer(
1618 wgpu::TexelCopyTextureInfo {
1619 texture,
1620 mip_level: 0,
1621 origin: wgpu::Origin3d::ZERO,
1622 aspect: wgpu::TextureAspect::All,
1623 },
1624 wgpu::TexelCopyBufferInfo {
1625 buffer,
1626 layout: wgpu::TexelCopyBufferLayout {
1627 offset,
1628 bytes_per_row: Some(bytes_per_row),
1629 rows_per_image: Some(size.height),
1630 },
1631 },
1632 wgpu::Extent3d {
1633 width: size.width,
1634 height: size.height,
1635 depth_or_array_layers: 1,
1636 },
1637 );
1638}
1639
1640#[cfg(test)]
1641pub(crate) fn read_rgba8(renderer: &Renderer, target: &RenderedTexture) -> Vec<u8> {
1642 renderer
1643 .read_rgba8(target)
1644 .expect("test texture readback succeeds")
1645}
1646
1647#[cfg(test)]
1648#[path = "readback_tests.rs"]
1649mod layout_tests;