1use crate::hdr::HdrToneMap;
2use crate::{
3 Color, ColorDiagnostic, ColorPolicy, RenderError, RenderSize, RenderedTexture, Renderer,
4 pipeline::{CachedSampledPipeline, CachedYuvPipeline},
5};
6use alpha2_media::{
7 ChromaLocation, ColorMatrix, ColorMetadata, ColorPrimaries, ColorRange, Orientation, Rect,
8 TransferFunction, YuvFormat,
9};
10use alpha2_shaders::{RGBA_PRESENT_WGSL, YUV_TO_LINEAR_WGSL, YUV16_TO_LINEAR_WGSL};
11use std::fmt::Debug;
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::sync::{Arc, Mutex};
14
15use crate::pool::UploadTexture;
16use crate::uniform::UniformArenaLease;
17
18#[derive(Debug)]
21pub struct PreparedSourceTexture {
22 texture: RenderedTexture,
23 pending: Option<PendingSourceWork>,
24}
25
26impl PreparedSourceTexture {
27 #[must_use]
29 pub fn into_parts(self) -> (RenderedTexture, Option<PendingSourceWork>) {
30 (self.texture, self.pending)
31 }
32
33 #[must_use]
35 pub fn retained_bytes(&self) -> u64 {
36 texture_bytes(self.texture.size, self.texture.format).saturating_add(
37 self.pending
38 .as_ref()
39 .map_or(0, PendingSourceWork::retained_bytes),
40 )
41 }
42}
43
44#[derive(Debug)]
46pub struct PendingSourceWork {
47 state: Mutex<Option<SourceWork>>,
48 retained_bytes: u64,
49 submitted: AtomicBool,
50}
51
52#[derive(Debug)]
53pub(crate) struct SourceWork {
54 pub(crate) label: &'static str,
55 pub(crate) conversion: bool,
58 pub(crate) pipeline: wgpu::RenderPipeline,
59 pub(crate) bind_group: wgpu::BindGroup,
60 pub(crate) output_view: wgpu::TextureView,
61 pub(crate) uniforms: UniformArenaLease,
62 pub(crate) uploads: Vec<UploadTexture>,
63 pub(crate) external_leases: Vec<Arc<dyn ExternalTextureLease>>,
64}
65
66pub trait ExternalTextureLease: Debug + Send + Sync + 'static {
69 fn retained_bytes(&self) -> u64;
71
72 fn prepare_submission(&self) -> bool {
74 true
75 }
76
77 fn flush_submission(&self) -> Result<(), RenderError> {
84 Ok(())
85 }
86
87 fn submitted(&self) {}
89
90 fn staged_signals(&self) -> usize {
98 0
99 }
100}
101
102#[derive(Debug, Default)]
107pub(crate) struct RetiredLeases {
108 state: Mutex<LeaseState>,
109 pub(crate) parks: crate::settle::CallbackSignal,
112 #[cfg(test)]
116 pub(crate) park_elsewhere_after: Mutex<Option<std::time::Duration>>,
117}
118
119#[derive(Debug, Default)]
120struct LeaseState {
121 parked: Vec<Arc<dyn ExternalTextureLease>>,
122 awaited: usize,
124}
125
126impl RetiredLeases {
127 fn lock(&self) -> std::sync::MutexGuard<'_, LeaseState> {
128 self.state
129 .lock()
130 .unwrap_or_else(std::sync::PoisonError::into_inner)
131 }
132
133 fn await_completion(&self) {
135 self.lock().awaited += 1;
136 }
137
138 fn park(&self, leases: Vec<Arc<dyn ExternalTextureLease>>) {
139 {
140 let mut state = self.lock();
141 state.parked.extend(leases);
142 state.awaited = state.awaited.saturating_sub(1);
143 }
144 self.parks.notify();
145 }
146
147 #[cfg(not(target_arch = "wasm32"))]
149 pub(crate) fn awaited(&self) -> usize {
150 self.lock().awaited
151 }
152
153 pub(crate) fn drain(&self) -> usize {
157 let leases = std::mem::take(&mut self.lock().parked);
158 let released = leases.len();
159 drop(leases);
160 released
161 }
162}
163
164pub(crate) struct SourceWorkTicket<'a> {
165 owner: &'a PendingSourceWork,
166 work: Option<SourceWork>,
167}
168
169impl PendingSourceWork {
170 pub(crate) fn new(work: SourceWork) -> Self {
171 let retained_bytes = work
172 .uploads
173 .iter()
174 .fold(0_u64, |total, input| {
175 total.saturating_add(input.retained_bytes())
176 })
177 .saturating_add(work.external_leases.iter().fold(0_u64, |total, lease| {
178 total.saturating_add(lease.retained_bytes())
179 }));
180 Self {
181 state: Mutex::new(Some(work)),
182 retained_bytes,
183 submitted: AtomicBool::new(false),
184 }
185 }
186
187 #[must_use]
189 pub fn retained_bytes(&self) -> u64 {
190 if self.submitted.load(Ordering::Acquire) {
191 0
192 } else {
193 self.retained_bytes
194 }
195 }
196
197 pub(crate) fn begin(
201 &self,
202 _frame: &crate::renderer::FrameGuard,
203 ) -> Option<SourceWorkTicket<'_>> {
204 let work = self
205 .state
206 .lock()
207 .unwrap_or_else(std::sync::PoisonError::into_inner)
208 .take()?;
209 Some(SourceWorkTicket {
210 owner: self,
211 work: Some(work),
212 })
213 }
214}
215
216impl SourceWorkTicket<'_> {
217 pub(crate) fn encode(&self, encoder: &mut wgpu::CommandEncoder) {
218 let work = self.work.as_ref().expect("live source work ticket");
219 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
220 label: Some(work.label),
221 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
222 view: &work.output_view,
223 depth_slice: None,
224 resolve_target: None,
225 ops: wgpu::Operations {
226 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
227 store: wgpu::StoreOp::Store,
228 },
229 })],
230 depth_stencil_attachment: None,
231 timestamp_writes: None,
232 occlusion_query_set: None,
233 multiview_mask: None,
234 });
235 pass.set_pipeline(&work.pipeline);
236 pass.set_bind_group(0, &work.bind_group, &[]);
237 pass.draw(0..3, 0..1);
238 }
239
240 pub(crate) fn flush_uniforms(&self, queue: &wgpu::Queue) {
241 self.work
242 .as_ref()
243 .expect("live source work ticket")
244 .uniforms
245 .flush(queue);
246 }
247
248 pub(crate) fn has_no_staged_signals(&self) -> bool {
250 self.work.as_ref().is_none_or(|work| {
251 work.external_leases
252 .iter()
253 .all(|lease| lease.staged_signals() == 0)
254 })
255 }
256
257 pub(crate) fn prepare_submission(&self) -> bool {
258 let work = self.work.as_ref().expect("live source work ticket");
259 work.external_leases
260 .iter()
261 .all(|lease| lease.prepare_submission())
262 }
263
264 pub(crate) fn flush_submission(&self) -> Result<(), RenderError> {
265 let work = self.work.as_ref().expect("live source work ticket");
266 work.external_leases
267 .iter()
268 .try_for_each(|lease| lease.flush_submission())
269 }
270
271 pub(crate) fn complete(mut self, renderer: &Renderer, standalone_submission: bool) {
272 let work = self.work.take().expect("live source work ticket");
273 let work_conversion = work.conversion;
274 work.uniforms.recycle();
275 drop(work.uploads);
276 if !work.external_leases.is_empty() {
277 let leases = work.external_leases;
278 for lease in &leases {
279 lease.submitted();
280 }
281 let retired = Arc::clone(&renderer.retired_leases);
284 retired.await_completion();
285 #[cfg(test)]
286 let elsewhere = *retired
287 .park_elsewhere_after
288 .lock()
289 .unwrap_or_else(std::sync::PoisonError::into_inner);
290 renderer.gpu.on_submitted_work_done(move || {
291 #[cfg(test)]
292 if let Some(delay) = elsewhere {
293 std::thread::spawn(move || {
294 std::thread::sleep(delay);
295 retired.park(leases);
296 });
297 return;
298 }
299 retired.park(leases);
300 });
301 }
302 self.owner.submitted.store(true, Ordering::Release);
303 crate::diagnostics::record_source_work(
304 &renderer.diagnostics,
305 1,
306 standalone_submission,
307 work_conversion,
308 );
309 }
310}
311
312impl Drop for SourceWorkTicket<'_> {
313 fn drop(&mut self) {
314 let Some(work) = self.work.take() else {
315 return;
316 };
317 let mut state = self
318 .owner
319 .state
320 .lock()
321 .unwrap_or_else(std::sync::PoisonError::into_inner);
322 debug_assert!(state.is_none());
323 *state = Some(work);
324 }
325}
326
327#[derive(Copy, Clone, Debug, Eq, PartialEq)]
329pub enum SourceAlpha {
330 Straight,
333 Premultiplied,
335}
336
337#[derive(Copy, Clone, Debug, Eq, PartialEq)]
339pub(crate) enum PresentedEncoding {
340 Srgb(SourceAlpha),
342 Linear,
345}
346
347#[derive(Copy, Clone, Debug, Eq, PartialEq)]
349pub struct SourcePresentation {
350 pub output_size: RenderSize,
352 pub visible_rect: Rect,
354 pub orientation: Orientation,
356 pub sample_aspect_ratio: (u32, u32),
358 pub chroma_siting: ChromaLocation,
360}
361
362impl SourcePresentation {
363 #[must_use]
365 pub const fn identity(size: RenderSize) -> Self {
366 Self {
367 output_size: size,
368 visible_rect: Rect {
369 x: 0,
370 y: 0,
371 width: size.width,
372 height: size.height,
373 },
374 orientation: Orientation::UPRIGHT,
375 sample_aspect_ratio: (1, 1),
376 chroma_siting: ChromaLocation::Unspecified,
377 }
378 }
379}
380
381#[derive(Copy, Clone, Debug)]
383pub struct Yuv420Frame<'a> {
384 pub y: &'a [u8],
386 pub u: &'a [u8],
388 pub v: &'a [u8],
390 pub y_stride: u32,
392 pub chroma_stride: u32,
394 pub size: RenderSize,
396 pub alpha: f32,
398 pub color: Option<ColorMetadata>,
400}
401
402impl<'a> From<Yuv420Frame<'a>> for YuvFrame<'a> {
403 fn from(frame: Yuv420Frame<'a>) -> Self {
404 Self {
405 format: YuvFormat::I420,
406 planes: [
407 SourcePlane {
408 bytes: frame.y,
409 stride: frame.y_stride,
410 },
411 SourcePlane {
412 bytes: frame.u,
413 stride: frame.chroma_stride,
414 },
415 SourcePlane {
416 bytes: frame.v,
417 stride: frame.chroma_stride,
418 },
419 ],
420 alpha_plane: None,
421 size: frame.size,
422 alpha: frame.alpha,
423 color: frame.color,
424 }
425 }
426}
427
428#[derive(Copy, Clone, Debug)]
430pub struct SourcePlane<'a> {
431 pub bytes: &'a [u8],
433 pub stride: u32,
435}
436
437#[derive(Copy, Clone, Debug)]
440pub struct YuvFrame<'a> {
441 pub format: YuvFormat,
443 pub planes: [SourcePlane<'a>; 3],
446 pub alpha_plane: Option<SourcePlane<'a>>,
449 pub size: RenderSize,
451 pub alpha: f32,
453 pub color: Option<ColorMetadata>,
455}
456
457#[derive(Copy, Clone, Debug, Eq, PartialEq)]
459enum PlaneStorage {
460 Unorm8,
462 Unorm16,
464 Uint16,
469}
470
471impl PlaneStorage {
472 fn texture_format(self, interleaved: bool) -> wgpu::TextureFormat {
473 match (self, interleaved) {
474 (Self::Unorm8, false) => wgpu::TextureFormat::R8Unorm,
475 (Self::Unorm8, true) => wgpu::TextureFormat::Rg8Unorm,
476 (Self::Unorm16, false) => wgpu::TextureFormat::R16Unorm,
477 (Self::Unorm16, true) => wgpu::TextureFormat::Rg16Unorm,
478 (Self::Uint16, false) => wgpu::TextureFormat::R16Uint,
479 (Self::Uint16, true) => wgpu::TextureFormat::Rg16Uint,
480 }
481 }
482
483 #[allow(clippy::cast_precision_loss)]
490 fn code_scale(self, format: YuvFormat) -> f32 {
491 match self {
492 Self::Unorm8 => 1.0,
493 Self::Unorm16 | Self::Uint16 => {
494 let maximum = ((1_u32 << format.bit_depth) - 1) as f32;
495 let alignment = if format.msb_aligned {
496 (1_u32 << (16 - u32::from(format.bit_depth))) as f32
497 } else {
498 1.0
499 };
500 65535.0 / (maximum * alignment)
501 }
502 }
503 }
504}
505
506#[derive(Copy, Clone, Debug)]
508#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
509pub struct GpuVideoPlanes<'a> {
510 pub y: &'a wgpu::TextureView,
512 pub chroma: &'a wgpu::TextureView,
514 pub v: Option<&'a wgpu::TextureView>,
516 pub format: YuvFormat,
519 pub size: RenderSize,
521 pub alpha: f32,
523 pub color: Option<ColorMetadata>,
525}
526
527impl Renderer {
528 pub fn prepare_rgba_presented(
534 &self,
535 source: UploadTexture,
536 source_size: RenderSize,
537 presentation: SourcePresentation,
538 alpha: SourceAlpha,
539 ) -> Result<PreparedSourceTexture, RenderError> {
540 let view = source.view().clone();
541 let prepared = self.prepare_rgba_presented_inner(
542 &view,
543 source_size,
544 presentation,
545 PresentedEncoding::Srgb(alpha),
546 vec![source],
547 )?;
548 Ok(prepared)
549 }
550
551 pub fn prepare_linear_presented(
562 &self,
563 source: UploadTexture,
564 source_size: RenderSize,
565 presentation: SourcePresentation,
566 ) -> Result<PreparedSourceTexture, RenderError> {
567 let view = source.view().clone();
568 self.prepare_rgba_presented_inner(
569 &view,
570 source_size,
571 presentation,
572 PresentedEncoding::Linear,
573 vec![source],
574 )
575 }
576
577 pub fn prepare_rgba8_presented(
584 &self,
585 pixels: SourcePlane<'_>,
586 size: RenderSize,
587 presentation: SourcePresentation,
588 ) -> Result<PreparedSourceTexture, RenderError> {
589 let texture = self.write_rgba8(size, pixels)?;
590 self.prepare_rgba_presented(texture, size, presentation, SourceAlpha::Straight)
591 }
592
593 pub(crate) fn prepare_rgba_presented_inner(
594 &self,
595 source_view: &wgpu::TextureView,
596 source_size: RenderSize,
597 presentation: SourcePresentation,
598 encoding: PresentedEncoding,
599 inputs: Vec<UploadTexture>,
600 ) -> Result<PreparedSourceTexture, RenderError> {
601 self.validate_size(source_size)?;
602 self.validate_size(presentation.output_size)?;
603 validate_presentation(source_size, presentation)?;
604 let output_format = self.intermediate_format();
605 let output = self.textures.acquire(
606 self.gpu.device(),
607 presentation.output_size,
608 output_format,
609 "alpha2 RGBA presentation target",
610 );
611 let cached = self.rgba_presentation_pipeline();
612 let uniform_bytes = presentation_uniform_bytes(
613 source_size,
614 presentation,
615 encoding,
616 self.presentation_filter_samples(),
617 );
618 let mut uniforms = self.uniform_buffers.acquire(
619 self.gpu.device(),
620 u64::try_from(uniform_bytes.len()).unwrap_or(u64::MAX),
621 );
622 let uniform = uniforms.write(&uniform_bytes);
623 let bind_group = self
624 .gpu
625 .device()
626 .create_bind_group(&wgpu::BindGroupDescriptor {
627 label: Some("alpha2 RGBA presentation bind group"),
628 layout: &cached.bind_group_layout,
629 entries: &[
630 wgpu::BindGroupEntry {
631 binding: 0,
632 resource: wgpu::BindingResource::TextureView(source_view),
633 },
634 wgpu::BindGroupEntry {
635 binding: 1,
636 resource: wgpu::BindingResource::Sampler(&cached.sampler),
637 },
638 wgpu::BindGroupEntry {
639 binding: 2,
640 resource: uniforms.binding(uniform),
641 },
642 ],
643 });
644 let output_view = output.view().clone();
645 Ok(PreparedSourceTexture {
646 texture: RenderedTexture::pooled(presentation.output_size, output_format, output),
647 pending: Some(PendingSourceWork::new(SourceWork {
648 label: "alpha2 RGBA presentation pass",
649 conversion: true,
650 pipeline: cached.pipeline.clone(),
651 bind_group,
652 output_view,
653 uniforms,
654 uploads: inputs,
655 external_leases: Vec::new(),
656 })),
657 })
658 }
659
660 pub fn submit_pending_source(&self, pending: &PendingSourceWork) -> bool {
667 let frame = self.frame_lock.hold();
668 let Some(ticket) = pending.begin(&frame) else {
669 return false;
670 };
671 let mut encoder = self.command_encoder("alpha2 source presentation encoder");
672 ticket.encode(&mut encoder);
673 ticket.flush_uniforms(self.gpu.queue());
674 if !ticket.prepare_submission() || ticket.flush_submission().is_err() {
675 return false;
676 }
677 if self.submit([encoder.finish()]).is_err() {
678 return false;
679 }
680 ticket.complete(self, true);
681 true
682 }
683
684 #[allow(clippy::too_many_lines)]
694 pub fn render_yuv420p(
695 &self,
696 frame: Yuv420Frame<'_>,
697 policy: ColorPolicy,
698 ) -> Result<(RenderedTexture, Vec<ColorDiagnostic>), RenderError> {
699 self.render_yuv420p_presented(frame, SourcePresentation::identity(frame.size), policy)
700 }
701
702 fn rgba_presentation_pipeline(&self) -> &CachedSampledPipeline {
703 self.pipelines.rgba_present.get_or_init(|| {
704 let output_format = self.intermediate_format();
705 let bind_group_layout =
706 self.gpu
707 .device()
708 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
709 label: Some("alpha2 RGBA presentation layout"),
710 entries: &[
711 wgpu::BindGroupLayoutEntry {
712 binding: 0,
713 visibility: wgpu::ShaderStages::FRAGMENT,
714 ty: wgpu::BindingType::Texture {
715 sample_type: wgpu::TextureSampleType::Float {
716 filterable: true,
717 },
718 view_dimension: wgpu::TextureViewDimension::D2,
719 multisampled: false,
720 },
721 count: None,
722 },
723 wgpu::BindGroupLayoutEntry {
724 binding: 1,
725 visibility: wgpu::ShaderStages::FRAGMENT,
726 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
727 count: None,
728 },
729 wgpu::BindGroupLayoutEntry {
730 binding: 2,
731 visibility: wgpu::ShaderStages::FRAGMENT,
732 ty: wgpu::BindingType::Buffer {
733 ty: wgpu::BufferBindingType::Uniform,
734 has_dynamic_offset: false,
735 min_binding_size: None,
736 },
737 count: None,
738 },
739 ],
740 });
741 let sampler = self.gpu.device().create_sampler(&wgpu::SamplerDescriptor {
742 label: Some("alpha2 RGBA presentation sampler"),
743 address_mode_u: wgpu::AddressMode::ClampToEdge,
744 address_mode_v: wgpu::AddressMode::ClampToEdge,
745 mag_filter: wgpu::FilterMode::Linear,
746 min_filter: wgpu::FilterMode::Linear,
747 ..Default::default()
748 });
749 let shader = self
750 .gpu
751 .device()
752 .create_shader_module(wgpu::ShaderModuleDescriptor {
753 label: Some("alpha2 RGBA presentation shader"),
754 source: wgpu::ShaderSource::Wgsl(RGBA_PRESENT_WGSL.into()),
755 });
756 let layout =
757 self.gpu
758 .device()
759 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
760 label: Some("alpha2 RGBA presentation pipeline layout"),
761 bind_group_layouts: &[Some(&bind_group_layout)],
762 immediate_size: 0,
763 });
764 let pipeline =
765 self.gpu
766 .device()
767 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
768 label: Some("alpha2 RGBA presentation pipeline"),
769 layout: Some(&layout),
770 vertex: wgpu::VertexState {
771 module: &shader,
772 entry_point: Some("vs"),
773 buffers: &[],
774 compilation_options: wgpu::PipelineCompilationOptions::default(),
775 },
776 fragment: Some(wgpu::FragmentState {
777 module: &shader,
778 entry_point: Some("fs"),
779 targets: &[Some(output_format.into())],
780 compilation_options: wgpu::PipelineCompilationOptions::default(),
781 }),
782 primitive: wgpu::PrimitiveState::default(),
783 depth_stencil: None,
784 multisample: wgpu::MultisampleState::default(),
785 multiview_mask: None,
786 cache: None,
787 });
788 CachedSampledPipeline {
789 bind_group_layout,
790 pipeline,
791 sampler,
792 }
793 })
794 }
795
796 #[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
807 pub fn render_yuv420p_presented(
808 &self,
809 frame: Yuv420Frame<'_>,
810 presentation: SourcePresentation,
811 policy: ColorPolicy,
812 ) -> Result<(RenderedTexture, Vec<ColorDiagnostic>), RenderError> {
813 let (prepared, diagnostics) =
814 self.prepare_yuv420p_presented(frame, presentation, policy)?;
815 let (texture, pending) = prepared.into_parts();
816 if let Some(pending) = pending {
817 self.submit_pending_source(&pending);
818 }
819 Ok((texture, diagnostics))
820 }
821
822 #[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
831 pub fn prepare_yuv420p_presented(
832 &self,
833 frame: Yuv420Frame<'_>,
834 presentation: SourcePresentation,
835 policy: ColorPolicy,
836 ) -> Result<(PreparedSourceTexture, Vec<ColorDiagnostic>), RenderError> {
837 self.prepare_yuv_presented(frame.into(), presentation, policy)
838 }
839
840 pub fn prepare_yuv_presented(
855 &self,
856 frame: YuvFrame<'_>,
857 presentation: SourcePresentation,
858 policy: ColorPolicy,
859 ) -> Result<(PreparedSourceTexture, Vec<ColorDiagnostic>), RenderError> {
860 let mut diagnostics = Vec::new();
861 let color = frame.color.unwrap_or_else(|| {
862 diagnostics.push(ColorDiagnostic::MissingMetadataFallback);
863 ColorMetadata::UNDECLARED
864 });
865 let tone = match hdr_handling(color, policy, &mut diagnostics)? {
866 HdrHandling::Sdr => None,
867 HdrHandling::ToneMap(tone) => Some(tone),
868 HdrHandling::Skip => {
869 return self
870 .render_solid(presentation.output_size, Color::rgba(0.0, 0.0, 0.0, 0.0))
871 .map(|texture| {
872 (
873 PreparedSourceTexture {
874 texture,
875 pending: None,
876 },
877 diagnostics,
878 )
879 });
880 }
881 };
882 let format = frame.format;
883 if !frame.alpha.is_finite()
884 || !(0.0..=1.0).contains(&frame.alpha)
885 || !format.is_supported()
886 || format.alpha != frame.alpha_plane.is_some()
887 {
888 return Err(RenderError::InvalidImageData);
889 }
890 self.validate_size(presentation.output_size)?;
891 validate_presentation(frame.size, presentation)?;
892 let storage = if format.bytes_per_sample() == 1 {
893 PlaneStorage::Unorm8
894 } else if self
895 .features
896 .contains(wgpu::Features::TEXTURE_FORMAT_16BIT_NORM)
897 {
898 PlaneStorage::Unorm16
899 } else {
900 PlaneStorage::Uint16
901 };
902 let (chroma_width, chroma_height) = format.chroma_size(frame.size.width, frame.size.height);
903 let pairs = if format.interleaved_chroma { 2 } else { 1 };
904 let plane_specs = [
905 ("alpha2 Y plane", frame.size.width, frame.size.height, 1_u32),
906 ("alpha2 U plane", chroma_width, chroma_height, pairs),
907 ("alpha2 V plane", chroma_width, chroma_height, 1),
908 ];
909 let mut uploads = Vec::with_capacity(format.plane_count());
910 let alpha_spec = ("alpha2 A plane", frame.size.width, frame.size.height, 1_u32);
911 for (plane, (label, width, height, samples)) in frame
912 .planes
913 .iter()
914 .zip(plane_specs)
915 .take(format.color_plane_count())
916 .chain(frame.alpha_plane.iter().map(|plane| (plane, alpha_spec)))
917 {
918 let row_bytes = width
919 .checked_mul(samples)
920 .and_then(|row| row.checked_mul(format.bytes_per_sample()))
921 .ok_or(RenderError::InvalidImageData)?;
922 validate_plane(plane.bytes, plane.stride, row_bytes, height)?;
923 uploads.push(upload_plane(
924 self,
925 label,
926 *plane,
927 (width, height),
928 storage.texture_format(samples == 2),
929 )?);
930 }
931 let mut views = uploads
932 .iter()
933 .map(|upload| upload.view().clone())
934 .collect::<Vec<_>>();
935 let alpha_view = format.alpha.then(|| views.pop()).flatten();
936 let planes = GpuVideoPlanes {
937 y: &views[0],
938 chroma: &views[1],
939 v: views.get(2),
940 format,
941 size: frame.size,
942 alpha: frame.alpha,
943 color: Some(color),
944 };
945 let prepared = self.prepare_gpu_yuv_presented_inner(
946 planes,
947 alpha_view.as_ref(),
948 presentation,
949 tone,
950 storage,
951 uploads,
952 Vec::new(),
953 )?;
954 Ok((prepared, diagnostics))
955 }
956
957 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
968 pub fn prepare_gpu_yuv_presented(
969 &self,
970 planes: GpuVideoPlanes<'_>,
971 presentation: SourcePresentation,
972 policy: ColorPolicy,
973 lease: Arc<dyn ExternalTextureLease>,
974 ) -> Result<(PreparedSourceTexture, Vec<ColorDiagnostic>), RenderError> {
975 let mut diagnostics = Vec::new();
976 let color = planes.color.unwrap_or_else(|| {
977 diagnostics.push(ColorDiagnostic::MissingMetadataFallback);
978 ColorMetadata::UNDECLARED
979 });
980 let tone = match hdr_handling(color, policy, &mut diagnostics)? {
981 HdrHandling::Sdr => None,
982 HdrHandling::ToneMap(tone) => Some(tone),
983 HdrHandling::Skip => {
984 return self
985 .render_solid(presentation.output_size, Color::rgba(0.0, 0.0, 0.0, 0.0))
986 .map(|texture| {
987 (
988 PreparedSourceTexture {
989 texture,
990 pending: None,
991 },
992 diagnostics,
993 )
994 });
995 }
996 };
997 let planes = GpuVideoPlanes {
998 color: Some(color),
999 ..planes
1000 };
1001 let storage = if planes.format.bytes_per_sample() == 1 {
1002 PlaneStorage::Unorm8
1003 } else {
1004 PlaneStorage::Unorm16
1005 };
1006 let prepared = self.prepare_gpu_yuv_presented_inner(
1007 planes,
1008 None,
1009 presentation,
1010 tone,
1011 storage,
1012 Vec::new(),
1013 vec![lease],
1014 )?;
1015 Ok((prepared, diagnostics))
1016 }
1017
1018 #[allow(clippy::too_many_arguments)]
1021 fn prepare_gpu_yuv_presented_inner(
1022 &self,
1023 planes: GpuVideoPlanes<'_>,
1024 alpha_plane: Option<&wgpu::TextureView>,
1025 presentation: SourcePresentation,
1026 tone: Option<HdrToneMap>,
1027 storage: PlaneStorage,
1028 uploads: Vec<UploadTexture>,
1029 external_leases: Vec<Arc<dyn ExternalTextureLease>>,
1030 ) -> Result<PreparedSourceTexture, RenderError> {
1031 if !planes.alpha.is_finite() || !(0.0..=1.0).contains(&planes.alpha) {
1032 return Err(RenderError::InvalidImageData);
1033 }
1034 if !planes.format.is_supported()
1035 || planes.format.interleaved_chroma == planes.v.is_some()
1036 || planes.format.alpha != alpha_plane.is_some()
1037 {
1038 return Err(RenderError::InvalidImageData);
1039 }
1040 self.validate_size(planes.size)?;
1041 self.validate_size(presentation.output_size)?;
1042 validate_presentation(planes.size, presentation)?;
1043 let color = planes.color.unwrap_or_default();
1044 let output_format = self.intermediate_format();
1045 let output = self.textures.acquire(
1046 self.gpu.device(),
1047 presentation.output_size,
1048 output_format,
1049 "alpha2 YUV conversion target",
1050 );
1051 let output_view = output.view().clone();
1052 let cached = self.yuv_pipeline(storage == PlaneStorage::Uint16);
1053 let uniform_bytes = uniform_bytes(
1054 color,
1055 tone,
1056 planes.alpha,
1057 planes.size,
1058 presentation,
1059 planes.format,
1060 storage.code_scale(planes.format),
1061 self.presentation_filter_samples(),
1062 );
1063 let mut uniforms = self.uniform_buffers.acquire(
1064 self.gpu.device(),
1065 u64::try_from(uniform_bytes.len()).unwrap_or(u64::MAX),
1066 );
1067 let uniform = uniforms.write(&uniform_bytes);
1068 let bind_group = self
1069 .gpu
1070 .device()
1071 .create_bind_group(&wgpu::BindGroupDescriptor {
1072 label: Some("alpha2 YUV bind group"),
1073 layout: &cached.bind_group_layout,
1074 entries: &[
1075 wgpu::BindGroupEntry {
1076 binding: 0,
1077 resource: wgpu::BindingResource::TextureView(planes.y),
1078 },
1079 wgpu::BindGroupEntry {
1080 binding: 1,
1081 resource: wgpu::BindingResource::TextureView(planes.chroma),
1082 },
1083 wgpu::BindGroupEntry {
1084 binding: 2,
1085 resource: wgpu::BindingResource::TextureView(
1086 planes.v.unwrap_or(planes.chroma),
1087 ),
1088 },
1089 wgpu::BindGroupEntry {
1090 binding: 3,
1091 resource: wgpu::BindingResource::Sampler(&cached.sampler),
1092 },
1093 wgpu::BindGroupEntry {
1094 binding: 4,
1095 resource: uniforms.binding(uniform),
1096 },
1097 wgpu::BindGroupEntry {
1098 binding: 5,
1099 resource: wgpu::BindingResource::TextureView(
1101 alpha_plane.unwrap_or(planes.y),
1102 ),
1103 },
1104 ],
1105 });
1106 Ok(PreparedSourceTexture {
1107 texture: RenderedTexture::pooled(presentation.output_size, output_format, output),
1108 pending: Some(PendingSourceWork::new(SourceWork {
1109 label: "alpha2 YUV conversion pass",
1110 conversion: true,
1111 pipeline: cached.pipeline.clone(),
1112 bind_group,
1113 output_view,
1114 uniforms,
1115 uploads,
1116 external_leases,
1117 })),
1118 })
1119 }
1120}
1121
1122pub(crate) fn validate_plane(
1123 bytes: &[u8],
1124 stride: u32,
1125 width: u32,
1126 height: u32,
1127) -> Result<(), RenderError> {
1128 let required = usize::try_from(stride).ok().and_then(|stride| {
1129 usize::try_from(height)
1130 .ok()
1131 .and_then(|height| stride.checked_mul(height))
1132 });
1133 if stride < width || required != Some(bytes.len()) {
1134 Err(RenderError::InvalidImageData)
1135 } else {
1136 Ok(())
1137 }
1138}
1139
1140fn texture_bytes(size: RenderSize, format: wgpu::TextureFormat) -> u64 {
1141 let block = format.block_copy_size(None).unwrap_or(4);
1142 u64::from(size.width)
1143 .saturating_mul(u64::from(size.height))
1144 .saturating_mul(u64::from(block))
1145}
1146
1147pub(crate) fn upload_plane(
1149 renderer: &Renderer,
1150 label: &'static str,
1151 plane: SourcePlane<'_>,
1152 (width, height): (u32, u32),
1153 texture_format: wgpu::TextureFormat,
1154) -> Result<crate::UploadTexture, RenderError> {
1155 let size = RenderSize::new(width, height)?;
1156 renderer.validate_size(size)?;
1157 let texture =
1158 renderer
1159 .upload_textures
1160 .acquire(renderer.gpu.device(), size, texture_format, label);
1161 renderer.gpu.queue().write_texture(
1162 wgpu::TexelCopyTextureInfo {
1163 texture: texture.texture(),
1164 mip_level: 0,
1165 origin: wgpu::Origin3d::ZERO,
1166 aspect: wgpu::TextureAspect::All,
1167 },
1168 plane.bytes,
1169 wgpu::TexelCopyBufferLayout {
1170 offset: 0,
1171 bytes_per_row: Some(plane.stride),
1172 rows_per_image: Some(height),
1173 },
1174 wgpu::Extent3d {
1175 width,
1176 height,
1177 depth_or_array_layers: 1,
1178 },
1179 );
1180 Ok(texture)
1181}
1182
1183impl Renderer {
1184 #[allow(clippy::cast_precision_loss)]
1188 fn presentation_filter_samples(&self) -> f32 {
1189 self.quality_controls().max_filter_samples as f32
1190 }
1191
1192 fn yuv_bind_group_layout(&self, sample_type: wgpu::TextureSampleType) -> wgpu::BindGroupLayout {
1195 let texture = |binding| wgpu::BindGroupLayoutEntry {
1196 binding,
1197 visibility: wgpu::ShaderStages::FRAGMENT,
1198 ty: wgpu::BindingType::Texture {
1199 sample_type,
1200 view_dimension: wgpu::TextureViewDimension::D2,
1201 multisampled: false,
1202 },
1203 count: None,
1204 };
1205 self.gpu
1206 .device()
1207 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1208 label: Some("alpha2 YUV layout"),
1209 entries: &[
1210 texture(0),
1211 texture(1),
1212 texture(2),
1213 wgpu::BindGroupLayoutEntry {
1214 binding: 3,
1215 visibility: wgpu::ShaderStages::FRAGMENT,
1216 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1217 count: None,
1218 },
1219 wgpu::BindGroupLayoutEntry {
1220 binding: 4,
1221 visibility: wgpu::ShaderStages::FRAGMENT,
1222 ty: wgpu::BindingType::Buffer {
1223 ty: wgpu::BufferBindingType::Uniform,
1224 has_dynamic_offset: false,
1225 min_binding_size: None,
1226 },
1227 count: None,
1228 },
1229 texture(5),
1230 ],
1231 })
1232 }
1233
1234 fn yuv_pipeline(&self, integer_planes: bool) -> &CachedYuvPipeline {
1237 let (cache, source) = if integer_planes {
1238 (&self.pipelines.yuv16, YUV16_TO_LINEAR_WGSL)
1239 } else {
1240 (&self.pipelines.yuv, YUV_TO_LINEAR_WGSL)
1241 };
1242 cache.get_or_init(|| {
1243 let bind_group_layout = self.yuv_bind_group_layout(if integer_planes {
1244 wgpu::TextureSampleType::Uint
1245 } else {
1246 wgpu::TextureSampleType::Float { filterable: true }
1247 });
1248 let shader = self
1249 .gpu
1250 .device()
1251 .create_shader_module(wgpu::ShaderModuleDescriptor {
1252 label: Some("alpha2 YUV conversion shader"),
1253 source: wgpu::ShaderSource::Wgsl(source.into()),
1254 });
1255 let pipeline_layout =
1256 self.gpu
1257 .device()
1258 .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1259 label: Some("alpha2 YUV pipeline layout"),
1260 bind_group_layouts: &[Some(&bind_group_layout)],
1261 immediate_size: 0,
1262 });
1263 let pipeline =
1264 self.gpu
1265 .device()
1266 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1267 label: Some("alpha2 YUV pipeline"),
1268 layout: Some(&pipeline_layout),
1269 vertex: wgpu::VertexState {
1270 module: &shader,
1271 entry_point: Some("vs"),
1272 buffers: &[],
1273 compilation_options: wgpu::PipelineCompilationOptions::default(),
1274 },
1275 fragment: Some(wgpu::FragmentState {
1276 module: &shader,
1277 entry_point: Some("fs_srgb"),
1278 targets: &[Some(self.intermediate_format().into())],
1279 compilation_options: wgpu::PipelineCompilationOptions::default(),
1280 }),
1281 primitive: wgpu::PrimitiveState::default(),
1282 depth_stencil: None,
1283 multisample: wgpu::MultisampleState::default(),
1284 multiview_mask: None,
1285 cache: None,
1286 });
1287 let sampler = self.gpu.device().create_sampler(&wgpu::SamplerDescriptor {
1288 label: Some("alpha2 YUV sampler"),
1289 address_mode_u: wgpu::AddressMode::ClampToEdge,
1290 address_mode_v: wgpu::AddressMode::ClampToEdge,
1291 mag_filter: wgpu::FilterMode::Linear,
1292 min_filter: wgpu::FilterMode::Linear,
1293 ..Default::default()
1294 });
1295 CachedYuvPipeline {
1296 bind_group_layout,
1297 pipeline,
1298 sampler,
1299 }
1300 })
1301 }
1302}
1303
1304fn validate_presentation(
1305 source: RenderSize,
1306 presentation: SourcePresentation,
1307) -> Result<(), RenderError> {
1308 let visible = presentation.visible_rect;
1309 if visible.width == 0
1310 || visible.height == 0
1311 || presentation.sample_aspect_ratio.0 == 0
1312 || presentation.sample_aspect_ratio.1 == 0
1313 || visible
1314 .x
1315 .checked_add(visible.width)
1316 .is_none_or(|right| right > source.width)
1317 || visible
1318 .y
1319 .checked_add(visible.height)
1320 .is_none_or(|bottom| bottom > source.height)
1321 {
1322 Err(RenderError::InvalidImageData)
1323 } else {
1324 Ok(())
1325 }
1326}
1327
1328enum HdrHandling {
1330 Sdr,
1332 ToneMap(HdrToneMap),
1334 Skip,
1336}
1337
1338fn hdr_handling(
1340 color: ColorMetadata,
1341 policy: ColorPolicy,
1342 diagnostics: &mut Vec<ColorDiagnostic>,
1343) -> Result<HdrHandling, RenderError> {
1344 if !crate::is_hdr(color) {
1345 return Ok(HdrHandling::Sdr);
1346 }
1347 match policy {
1348 ColorPolicy::RejectHdr => {
1349 diagnostics.push(ColorDiagnostic::UnsupportedHdr);
1350 Err(RenderError::UnsupportedHdr)
1351 }
1352 ColorPolicy::WarnHdr => {
1353 diagnostics.push(ColorDiagnostic::UnsupportedHdr);
1354 Ok(HdrHandling::Skip)
1355 }
1356 ColorPolicy::ToneMapHdr(tone) => {
1357 diagnostics.push(ColorDiagnostic::ToneMappedHdr);
1358 Ok(HdrHandling::ToneMap(tone))
1359 }
1360 }
1361}
1362
1363fn orientation_uniform(orientation: Orientation) -> u32 {
1366 orientation.rotation.quarter_turns() | (u32::from(orientation.mirror) << 2)
1367}
1368
1369#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
1370fn presentation_uniform_bytes(
1371 source: RenderSize,
1372 presentation: SourcePresentation,
1373 encoding: PresentedEncoding,
1374 max_filter_samples: f32,
1375) -> Vec<u8> {
1376 let visible = presentation.visible_rect;
1377 let sar = f64::from(presentation.sample_aspect_ratio.0)
1378 / f64::from(presentation.sample_aspect_ratio.1);
1379 let unrotated_width = f64::from(visible.width) * sar;
1380 let unrotated_height = f64::from(visible.height);
1381 let (display_width, display_height) = if presentation.orientation.rotation.swaps_axes() {
1382 (unrotated_height, unrotated_width)
1383 } else {
1384 (unrotated_width, unrotated_height)
1385 };
1386 let scale = (f64::from(presentation.output_size.width) / display_width)
1387 .min(f64::from(presentation.output_size.height) / display_height);
1388 let draw_width = display_width * scale;
1389 let draw_height = display_height * scale;
1390 let draw_x = (f64::from(presentation.output_size.width) - draw_width) * 0.5;
1391 let draw_y = (f64::from(presentation.output_size.height) - draw_height) * 0.5;
1392 let values = [
1393 presentation.output_size.width as f32,
1394 presentation.output_size.height as f32,
1395 source.width as f32,
1396 source.height as f32,
1397 visible.x as f32,
1398 visible.y as f32,
1399 visible.width as f32,
1400 visible.height as f32,
1401 draw_x as f32,
1402 draw_y as f32,
1403 draw_width as f32,
1404 draw_height as f32,
1405 ];
1406 let mut bytes = Vec::with_capacity(64);
1407 for value in values {
1408 bytes.extend_from_slice(&value.to_ne_bytes());
1409 }
1410 bytes.extend_from_slice(&orientation_uniform(presentation.orientation).to_ne_bytes());
1411 let mode = match encoding {
1412 PresentedEncoding::Srgb(SourceAlpha::Straight) => 0_u32,
1413 PresentedEncoding::Srgb(SourceAlpha::Premultiplied) => 1,
1414 PresentedEncoding::Linear => 2,
1415 };
1416 bytes.extend_from_slice(&mode.to_ne_bytes());
1417 bytes.extend_from_slice(&max_filter_samples.to_ne_bytes());
1418 bytes.extend_from_slice(&[0; 4]);
1419 bytes
1420}
1421
1422#[allow(
1423 clippy::cast_possible_truncation,
1424 clippy::cast_precision_loss,
1425 clippy::too_many_arguments
1426)]
1427fn uniform_bytes(
1428 color: ColorMetadata,
1429 tone: Option<HdrToneMap>,
1430 alpha: f32,
1431 source: RenderSize,
1432 presentation: SourcePresentation,
1433 format: YuvFormat,
1434 code_scale: f32,
1435 max_filter_samples: f32,
1436) -> Vec<u8> {
1437 let mut bytes = color_uniform_bytes(color, tone, alpha, format.bit_depth, code_scale);
1438 let (alpha_plane, alpha_scale) = if format.alpha {
1441 (1_u32, code_scale)
1442 } else {
1443 (0, 0.0)
1444 };
1445 bytes[88..92].copy_from_slice(&alpha_plane.to_ne_bytes());
1446 bytes[92..96].copy_from_slice(&alpha_scale.to_ne_bytes());
1447 let visible = presentation.visible_rect;
1448 let sar = f64::from(presentation.sample_aspect_ratio.0)
1449 / f64::from(presentation.sample_aspect_ratio.1);
1450 let unrotated_width = f64::from(visible.width) * sar;
1451 let unrotated_height = f64::from(visible.height);
1452 let (display_width, display_height) = if presentation.orientation.rotation.swaps_axes() {
1453 (unrotated_height, unrotated_width)
1454 } else {
1455 (unrotated_width, unrotated_height)
1456 };
1457 let scale = (f64::from(presentation.output_size.width) / display_width)
1458 .min(f64::from(presentation.output_size.height) / display_height);
1459 let draw_width = display_width * scale;
1460 let draw_height = display_height * scale;
1461 let draw_x = (f64::from(presentation.output_size.width) - draw_width) * 0.5;
1462 let draw_y = (f64::from(presentation.output_size.height) - draw_height) * 0.5;
1463 let (chroma_width, chroma_height) = format.chroma_size(source.width, source.height);
1464 let chroma_size = [chroma_width as f32, chroma_height as f32];
1465 let factor_x = (1_u32 << format.chroma_shift_x) as f32;
1466 let factor_y = (1_u32 << format.chroma_shift_y) as f32;
1467 let (cosited_x, cosited_y) = match presentation.chroma_siting {
1469 ChromaLocation::Left => (true, false),
1470 ChromaLocation::TopLeft => (true, true),
1471 ChromaLocation::Unspecified | ChromaLocation::Center => (false, false),
1472 };
1473 let chroma_offset = [
1474 if cosited_x { factor_x * 0.5 - 0.5 } else { 0.0 },
1475 if cosited_y { factor_y * 0.5 - 0.5 } else { 0.0 },
1476 ];
1477 let presentation_values = [
1478 presentation.output_size.width as f32,
1479 presentation.output_size.height as f32,
1480 source.width as f32,
1481 source.height as f32,
1482 visible.x as f32,
1483 visible.y as f32,
1484 visible.width as f32,
1485 visible.height as f32,
1486 draw_x as f32,
1487 draw_y as f32,
1488 draw_width as f32,
1489 draw_height as f32,
1490 chroma_size[0],
1491 chroma_size[1],
1492 chroma_offset[0],
1493 chroma_offset[1],
1494 ];
1495 for value in presentation_values {
1496 bytes.extend_from_slice(&value.to_ne_bytes());
1497 }
1498 bytes.extend_from_slice(&orientation_uniform(presentation.orientation).to_ne_bytes());
1499 let plane_layout = u32::from(format.interleaved_chroma);
1500 bytes.extend_from_slice(&plane_layout.to_ne_bytes());
1501 for scale in [1.0 / factor_x, 1.0 / factor_y] {
1502 bytes.extend_from_slice(&scale.to_ne_bytes());
1503 }
1504 bytes.extend(hdr_uniform_bytes(color, tone));
1505 for bound in chroma_window(visible, format) {
1506 bytes.extend_from_slice(&bound.to_ne_bytes());
1507 }
1508 for value in [max_filter_samples, 0.0, 0.0, 0.0] {
1509 bytes.extend_from_slice(&value.to_ne_bytes());
1510 }
1511 bytes
1512}
1513
1514#[allow(clippy::cast_precision_loss)]
1521fn chroma_window(visible: Rect, format: YuvFormat) -> [f32; 4] {
1522 let first = |origin: u32, shift: u8| (origin >> shift) as f32 + 0.5;
1523 let last =
1524 |origin: u32, length: u32, shift: u8| (origin + length).div_ceil(1 << shift) as f32 - 0.5;
1525 [
1526 first(visible.x, format.chroma_shift_x),
1527 first(visible.y, format.chroma_shift_y),
1528 last(visible.x, visible.width, format.chroma_shift_x),
1529 last(visible.y, visible.height, format.chroma_shift_y),
1530 ]
1531}
1532
1533fn hdr_uniform_bytes(color: ColorMetadata, tone: Option<HdrToneMap>) -> Vec<u8> {
1536 let gamut = crate::hdr::gamut_matrix(color.primaries, ColorPrimaries::Bt709);
1537 let tone = tone.unwrap_or_default();
1538 #[allow(clippy::cast_precision_loss)]
1539 let peak = tone.source_peak_nits as f32;
1540 let white = crate::hdr::SDR_WHITE_NITS;
1541 let gamma = 1.2 + 0.42 * (peak / 1_000.0).log10();
1542 let mut bytes = Vec::with_capacity(80);
1543 for value in gamut
1544 .into_iter()
1545 .flat_map(|row| [row[0], row[1], row[2], 0.0])
1546 .chain([
1547 peak,
1548 white,
1549 crate::hdr::pq_inverse_eotf(peak),
1550 crate::hdr::pq_inverse_eotf(white),
1551 gamma,
1552 0.0,
1553 0.0,
1554 0.0,
1555 ])
1556 {
1557 bytes.extend_from_slice(&value.to_ne_bytes());
1558 }
1559 bytes
1560}
1561
1562fn color_uniform_bytes(
1563 color: ColorMetadata,
1564 tone: Option<HdrToneMap>,
1565 alpha: f32,
1566 bit_depth: u8,
1567 code_scale: f32,
1568) -> Vec<u8> {
1569 let transfer: u32 = match (tone, color.transfer) {
1572 (Some(_), TransferFunction::Pq) => 1,
1573 (Some(_), TransferFunction::Hlg) => 2,
1574 _ => 0,
1575 };
1576 if color.matrix == ColorMatrix::Rgb {
1577 return identity_rgb_uniform_bytes(alpha, transfer);
1578 }
1579 let (kr, kb) = color
1580 .matrix
1581 .luma_weights()
1582 .expect("RGB components are handled above");
1583 let kg = 1.0 - kr - kb;
1584 let rows = [
1585 [1.0, 0.0, 2.0 * (1.0 - kr), 0.0],
1586 [
1587 1.0,
1588 -2.0 * kb * (1.0 - kb) / kg,
1589 -2.0 * kr * (1.0 - kr) / kg,
1590 0.0,
1591 ],
1592 [1.0, 2.0 * (1.0 - kb), 0.0, 0.0],
1593 ];
1594 let (scale, offset) = range_scale_offset(color.range, bit_depth, code_scale);
1595 let mut bytes = Vec::with_capacity(96);
1596 for value in rows.into_iter().flatten().chain(scale).chain(offset) {
1597 bytes.extend_from_slice(&value.to_ne_bytes());
1598 }
1599 bytes.extend_from_slice(&transfer.to_ne_bytes());
1600 bytes.extend_from_slice(&alpha.to_ne_bytes());
1601 bytes.extend_from_slice(&[0; 8]);
1602 bytes
1603}
1604
1605fn range_scale_offset(range: ColorRange, depth: u8, code: f32) -> ([f32; 4], [f32; 4]) {
1612 let levels = range.levels(depth);
1613 let (luma_scale, chroma_scale) = match range {
1614 ColorRange::Full => (code, code),
1615 ColorRange::Limited => (
1616 code * levels.maximum / levels.luma_span,
1617 code * levels.maximum / levels.chroma_span,
1618 ),
1619 };
1620 let chroma_offset = 0.0 - levels.chroma_center / levels.chroma_span;
1621 (
1622 [luma_scale, chroma_scale, chroma_scale, 0.0],
1623 [
1624 0.0 - levels.luma_black / levels.luma_span,
1625 chroma_offset,
1626 chroma_offset,
1627 0.0,
1628 ],
1629 )
1630}
1631
1632fn identity_rgb_uniform_bytes(alpha: f32, transfer: u32) -> Vec<u8> {
1633 let rows = [
1634 [1.0_f32, 0.0, 0.0, 0.0],
1635 [0.0_f32, 1.0, 0.0, 0.0],
1636 [0.0_f32, 0.0, 1.0, 0.0],
1637 ];
1638 let mut bytes = Vec::with_capacity(96);
1639 for value in rows
1640 .into_iter()
1641 .flatten()
1642 .chain([1.0, 1.0, 1.0, 0.0])
1643 .chain([0.0; 4])
1644 {
1645 bytes.extend_from_slice(&value.to_ne_bytes());
1646 }
1647 bytes.extend_from_slice(&transfer.to_ne_bytes());
1648 bytes.extend_from_slice(&alpha.to_ne_bytes());
1649 bytes.extend_from_slice(&[0; 8]);
1650 bytes
1651}
1652
1653#[cfg(test)]
1654mod tests {
1655 use super::*;
1656 use alpha2_media::{ColorPrimaries, DynamicRange};
1657
1658 #[test]
1659 fn parked_leases_are_dropped_only_when_drained() {
1660 #[derive(Debug)]
1663 struct CountingLease(Arc<std::sync::atomic::AtomicUsize>);
1664
1665 impl Drop for CountingLease {
1666 fn drop(&mut self) {
1667 self.0.fetch_add(1, Ordering::Release);
1668 }
1669 }
1670
1671 impl ExternalTextureLease for CountingLease {
1672 fn retained_bytes(&self) -> u64 {
1673 0
1674 }
1675 }
1676
1677 let drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1678 let retired = RetiredLeases::default();
1679 let lease: Arc<dyn ExternalTextureLease> = Arc::new(CountingLease(Arc::clone(&drops)));
1680 retired.park(vec![lease]);
1681 assert_eq!(drops.load(Ordering::Acquire), 0, "parking must not release");
1682 retired.drain();
1683 assert_eq!(drops.load(Ordering::Acquire), 1, "drain must release");
1684 retired.drain();
1685 assert_eq!(drops.load(Ordering::Acquire), 1, "drain must be idempotent");
1686 }
1687
1688 #[test]
1689 fn ten_bit_limited_range_normalizes_the_p010_container() {
1690 let (scale, offset) = range_scale_offset(
1691 ColorRange::Limited,
1692 10,
1693 PlaneStorage::Unorm16.code_scale(YuvFormat::P010),
1694 );
1695 let black = f32::from(64_u16 << 6) / 65535.0;
1697 assert!((black * scale[0] + offset[0]).abs() < 1e-4);
1698 let white = f32::from(940_u16 << 6) / 65535.0;
1700 assert!((white * scale[0] + offset[0] - 1.0).abs() < 1e-4);
1701 let neutral = f32::from(512_u16 << 6) / 65535.0;
1703 assert!((neutral * scale[1] + offset[1]).abs() < 1e-4);
1704 }
1705
1706 #[test]
1707 fn integer_containers_take_the_scale_normalized_ones_do() {
1708 for format in [YuvFormat::planar(1, 1, 10), YuvFormat::P010] {
1711 assert_eq!(
1712 PlaneStorage::Uint16.code_scale(format).to_bits(),
1713 PlaneStorage::Unorm16.code_scale(format).to_bits()
1714 );
1715 }
1716 assert_eq!(
1717 PlaneStorage::Uint16.texture_format(false),
1718 wgpu::TextureFormat::R16Uint
1719 );
1720 assert_eq!(
1721 PlaneStorage::Uint16.texture_format(true),
1722 wgpu::TextureFormat::Rg16Uint
1723 );
1724 }
1725
1726 #[test]
1727 fn eight_bit_range_constants_are_unchanged() {
1728 let bits = |values: [f32; 4]| values.map(f32::to_bits);
1729 let (scale, offset) = range_scale_offset(ColorRange::Limited, 8, 1.0);
1730 assert_eq!(
1731 bits(scale),
1732 bits([255.0 / 219.0, 255.0 / 224.0, 255.0 / 224.0, 0.0])
1733 );
1734 assert_eq!(
1735 bits(offset),
1736 bits([-16.0 / 219.0, -128.0 / 224.0, -128.0 / 224.0, 0.0])
1737 );
1738 let (scale, offset) = range_scale_offset(ColorRange::Full, 8, 1.0);
1739 assert_eq!(bits(scale), bits([1.0, 1.0, 1.0, 0.0]));
1740 assert_eq!(
1741 bits(offset),
1742 bits([0.0, -128.0 / 255.0, -128.0 / 255.0, 0.0])
1743 );
1744 }
1745
1746 #[test]
1747 fn rgb_uniform_uses_identity_matrix_without_chroma_offsets() {
1748 let bytes = uniform_bytes(
1749 ColorMetadata {
1750 primaries: ColorPrimaries::Bt709,
1751 transfer: TransferFunction::Srgb,
1752 matrix: ColorMatrix::Rgb,
1753 range: ColorRange::Limited,
1754 dynamic_range: DynamicRange::Sdr,
1755 },
1756 None,
1757 1.0,
1758 RenderSize::new(2, 2).expect("size"),
1759 SourcePresentation::identity(RenderSize::new(2, 2).expect("size")),
1760 YuvFormat::I420,
1761 1.0,
1762 64.0,
1763 );
1764 let floats = bytes[..80]
1765 .as_chunks::<4>()
1766 .0
1767 .iter()
1768 .map(|bytes| f32::from_ne_bytes(*bytes))
1769 .collect::<Vec<_>>();
1770 assert_eq!(
1771 floats.into_iter().map(f32::to_bits).collect::<Vec<_>>(),
1772 vec![
1773 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, ]
1779 .into_iter()
1780 .map(f32::to_bits)
1781 .collect::<Vec<_>>()
1782 );
1783 }
1784
1785 #[test]
1786 fn the_chroma_window_is_the_visible_pictures_chroma() {
1787 let window = |x, y, width, height, format| {
1788 chroma_window(
1789 Rect {
1790 x,
1791 y,
1792 width,
1793 height,
1794 },
1795 format,
1796 )
1797 .map(f32::to_bits)
1798 };
1799 let bits = |bounds: [f32; 4]| bounds.map(f32::to_bits);
1800 assert_eq!(
1802 window(0, 0, 1920, 1080, YuvFormat::I420),
1803 bits([0.5, 0.5, 959.5, 539.5])
1804 );
1805 assert_eq!(
1807 window(0, 0, 1366, 768, YuvFormat::I420),
1808 bits([0.5, 0.5, 682.5, 383.5])
1809 );
1810 assert_eq!(
1812 window(1, 3, 5, 4, YuvFormat::I420),
1813 bits([0.5, 1.5, 2.5, 3.5])
1814 );
1815 assert_eq!(
1816 window(2, 1, 3, 2, YuvFormat::planar(0, 0, 8)),
1817 bits([2.5, 1.5, 4.5, 2.5])
1818 );
1819 }
1820}