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