1use image::GenericImageView;
2use wgpu::util::DeviceExt;
3
4use crate::video::{GpuSelection, VideoFrameData, YuvColorInfo, YuvMatrix, YuvRange};
5
6const MAX_TEXTURE_BYTES: u64 = 256 * 1024 * 1024;
7const MAX_STATIC_DECODE_BYTES: u64 = 512 * 1024 * 1024;
8
9pub struct Renderer {
10 pub instance: wgpu::Instance,
11 pub adapter: wgpu::Adapter,
12 pub device: wgpu::Device,
13 pub queue: wgpu::Queue,
14 pub bind_group_layout_tex: wgpu::BindGroupLayout,
15 pub bind_group_layout_uni: wgpu::BindGroupLayout,
16 pipeline_layout: wgpu::PipelineLayout,
17 shader: wgpu::ShaderModule,
18 pipeline:
23 std::sync::Mutex<std::collections::HashMap<wgpu::TextureFormat, wgpu::RenderPipeline>>,
24 nv12_bind_group_layout: wgpu::BindGroupLayout,
25 nv12_pipeline: wgpu::RenderPipeline,
26 static_sampler: wgpu::Sampler,
30 video_plane_sampler: wgpu::Sampler,
31 video_output_sampler: wgpu::Sampler,
32}
33
34pub struct VideoTexture {
35 output: wgpu::Texture,
36 output_view: wgpu::TextureView,
37 effects_bind_group: wgpu::BindGroup,
38 luma: wgpu::Texture,
39 chroma: wgpu::Texture,
40 conversion_buffer: wgpu::Buffer,
41 conversion_bind_group: wgpu::BindGroup,
42 width: u32,
43 height: u32,
44}
45
46impl VideoTexture {
47 pub fn texture(&self) -> &wgpu::Texture {
48 &self.output
49 }
50
51 pub fn bind_group(&self) -> &wgpu::BindGroup {
52 &self.effects_bind_group
53 }
54
55 pub fn width(&self) -> u32 {
56 self.width
57 }
58
59 pub fn height(&self) -> u32 {
60 self.height
61 }
62
63 pub fn matches_dimensions(&self, width: u32, height: u32) -> bool {
67 self.width == width && self.height == height
68 }
69}
70
71#[repr(C)]
72#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
73struct YuvConversion {
74 range: [f32; 4],
75 red: [f32; 4],
76 green: [f32; 4],
77 blue: [f32; 4],
78}
79
80impl YuvConversion {
81 fn new(color: YuvColorInfo) -> Self {
82 let range = match color.range {
83 YuvRange::Limited => [16.0 / 255.0, 255.0 / 219.0, 128.0 / 255.0, 255.0 / 224.0],
84 YuvRange::Full => [0.0, 1.0, 128.0 / 255.0, 1.0],
85 };
86 let (red_cr, green_cb, green_cr, blue_cb) = match color.matrix {
87 YuvMatrix::Bt601 => (1.402, -0.344_136, -0.714_136, 1.772),
88 YuvMatrix::Bt709 => (1.5748, -0.187_324, -0.468_124, 1.8556),
89 YuvMatrix::Bt2020 => (1.4746, -0.164_553, -0.571_353, 1.8814),
90 };
91 Self {
92 range,
93 red: [1.0, 0.0, red_cr, 0.0],
94 green: [1.0, green_cb, green_cr, 0.0],
95 blue: [1.0, blue_cb, 0.0, 0.0],
96 }
97 }
98}
99
100pub struct PerOutputUniforms {
103 pub buffer: wgpu::Buffer,
104 pub bind_group: wgpu::BindGroup,
105}
106
107#[repr(C)]
108#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
109pub struct Uniforms {
110 pub time: f32,
111 pub progress: f32,
112 pub effect_type: u32,
113 pub padding: u32,
114 pub resolution: [f32; 2],
115 pub image_resolution: [f32; 2],
116 pub old_image_resolution: [f32; 2],
117 pub param_a: f32,
118 pub param_b: f32,
119 pub param_c: f32,
120 pub param_d: f32,
121 pub origin: [f32; 2],
122 pub direction: [f32; 2],
123 pub easing: u32,
124 pub scaling_mode: u32,
125}
126
127impl Uniforms {
128 pub fn from_effect(effect: &crate::animation::EffectUniforms) -> Self {
129 Self {
130 time: effect.progress,
131 progress: effect.progress,
132 effect_type: effect.effect_type,
133 padding: 0,
134 resolution: [1920.0, 1080.0],
135 image_resolution: [1920.0, 1080.0],
136 old_image_resolution: [1920.0, 1080.0],
137 param_a: effect.param_a,
138 param_b: effect.param_b,
139 param_c: effect.param_c,
140 param_d: effect.param_d,
141 origin: effect.origin,
142 direction: effect.direction,
143 easing: effect.easing,
144 scaling_mode: 0,
145 }
146 }
147}
148
149impl Default for Uniforms {
150 fn default() -> Self {
151 Self {
152 time: 0.0,
153 progress: 0.0,
154 effect_type: 0,
155 padding: 0,
156 resolution: [1920.0, 1080.0],
157 image_resolution: [1920.0, 1080.0],
158 old_image_resolution: [1920.0, 1080.0],
159 param_a: 0.0,
160 param_b: 0.0,
161 param_c: 0.0,
162 param_d: 0.0,
163 origin: [0.5, 0.5],
164 direction: [0.0, 0.0],
165 easing: 3,
166 scaling_mode: 0,
167 }
168 }
169}
170
171impl Renderer {
172 pub async fn new(preference: &GpuSelection) -> anyhow::Result<Self> {
173 let (instance, adapter) = 'select: {
178 #[cfg(target_os = "linux")]
182 {
183 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
184 backends: wgpu::Backends::VULKAN,
185 ..wgpu::InstanceDescriptor::new_without_display_handle()
186 });
187 if let Ok(adapter) = crate::video::select_adapter(&instance, preference).await {
188 break 'select (instance, adapter);
189 }
190 tracing::info!("No Vulkan adapter found, falling back to GL");
191 }
192 #[cfg(target_os = "linux")]
195 let backends = wgpu::Backends::VULKAN | wgpu::Backends::GL;
196 #[cfg(not(target_os = "linux"))]
197 let backends = wgpu::Backends::all();
198 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
199 backends,
200 ..wgpu::InstanceDescriptor::new_without_display_handle()
201 });
202 let adapter = crate::video::select_adapter(&instance, preference)
203 .await
204 .map_err(|e| anyhow::anyhow!(e))?;
205 (instance, adapter)
206 };
207
208 let adapter_limits = adapter.limits();
209 let required_limits = wgpu::Limits {
210 max_texture_dimension_2d: adapter_limits.max_texture_dimension_2d,
211 ..wgpu::Limits::default()
212 };
213
214 let (device, queue) = adapter
215 .request_device(&wgpu::DeviceDescriptor {
216 label: None,
217 required_features: wgpu::Features::empty(),
218 required_limits,
219 experimental_features: wgpu::ExperimentalFeatures::disabled(),
220 memory_hints: wgpu::MemoryHints::MemoryUsage,
225 trace: wgpu::Trace::default(),
226 })
227 .await?;
228
229 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
230 label: Some("Effects Shader"),
231 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
232 crate::shader::EFFECTS_SHADER,
233 )),
234 });
235
236 let bind_group_layout_tex =
237 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
238 entries: &[
239 wgpu::BindGroupLayoutEntry {
240 binding: 0,
241 visibility: wgpu::ShaderStages::FRAGMENT,
242 ty: wgpu::BindingType::Texture {
243 multisampled: false,
244 view_dimension: wgpu::TextureViewDimension::D2,
245 sample_type: wgpu::TextureSampleType::Float { filterable: true },
246 },
247 count: None,
248 },
249 wgpu::BindGroupLayoutEntry {
250 binding: 1,
251 visibility: wgpu::ShaderStages::FRAGMENT,
252 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
253 count: None,
254 },
255 ],
256 label: Some("texture_bind_group_layout"),
257 });
258
259 let bind_group_layout_uni =
260 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
261 entries: &[wgpu::BindGroupLayoutEntry {
262 binding: 0,
263 visibility: wgpu::ShaderStages::FRAGMENT,
264 ty: wgpu::BindingType::Buffer {
265 ty: wgpu::BufferBindingType::Uniform,
266 has_dynamic_offset: false,
267 min_binding_size: None,
268 },
269 count: None,
270 }],
271 label: Some("uniform_bind_group_layout"),
272 });
273
274 let nv12_bind_group_layout =
275 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
276 label: Some("NV12 Conversion Bind Group Layout"),
277 entries: &[
278 wgpu::BindGroupLayoutEntry {
279 binding: 0,
280 visibility: wgpu::ShaderStages::FRAGMENT,
281 ty: wgpu::BindingType::Texture {
282 multisampled: false,
283 view_dimension: wgpu::TextureViewDimension::D2,
284 sample_type: wgpu::TextureSampleType::Float { filterable: true },
285 },
286 count: None,
287 },
288 wgpu::BindGroupLayoutEntry {
289 binding: 1,
290 visibility: wgpu::ShaderStages::FRAGMENT,
291 ty: wgpu::BindingType::Texture {
292 multisampled: false,
293 view_dimension: wgpu::TextureViewDimension::D2,
294 sample_type: wgpu::TextureSampleType::Float { filterable: true },
295 },
296 count: None,
297 },
298 wgpu::BindGroupLayoutEntry {
299 binding: 2,
300 visibility: wgpu::ShaderStages::FRAGMENT,
301 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
302 count: None,
303 },
304 wgpu::BindGroupLayoutEntry {
305 binding: 3,
306 visibility: wgpu::ShaderStages::FRAGMENT,
307 ty: wgpu::BindingType::Buffer {
308 ty: wgpu::BufferBindingType::Uniform,
309 has_dynamic_offset: false,
310 min_binding_size: None,
311 },
312 count: None,
313 },
314 ],
315 });
316
317 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
318 label: Some("Render Pipeline Layout"),
319 bind_group_layouts: &[
320 Some(&bind_group_layout_tex),
321 Some(&bind_group_layout_tex),
322 Some(&bind_group_layout_uni),
323 ],
324 immediate_size: 0,
325 });
326
327 let nv12_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
328 label: Some("NV12 to RGB Shader"),
329 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
330 crate::shader::NV12_TO_RGB_SHADER,
331 )),
332 });
333 let nv12_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
334 label: Some("NV12 Conversion Pipeline Layout"),
335 bind_group_layouts: &[Some(&nv12_bind_group_layout)],
336 immediate_size: 0,
337 });
338 let nv12_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
339 label: Some("NV12 Conversion Pipeline"),
340 layout: Some(&nv12_pipeline_layout),
341 vertex: wgpu::VertexState {
342 module: &nv12_shader,
343 entry_point: Some("vs_main"),
344 buffers: &[],
345 compilation_options: wgpu::PipelineCompilationOptions::default(),
346 },
347 fragment: Some(wgpu::FragmentState {
348 module: &nv12_shader,
349 entry_point: Some("fs_main"),
350 targets: &[Some(wgpu::ColorTargetState {
351 format: wgpu::TextureFormat::Rgba8UnormSrgb,
352 blend: Some(wgpu::BlendState::REPLACE),
353 write_mask: wgpu::ColorWrites::ALL,
354 })],
355 compilation_options: wgpu::PipelineCompilationOptions::default(),
356 }),
357 primitive: wgpu::PrimitiveState::default(),
358 depth_stencil: None,
359 multisample: wgpu::MultisampleState::default(),
360 multiview_mask: None,
361 cache: None,
362 });
363
364 let static_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
365 label: Some("wallr static sampler"),
366 address_mode_u: wgpu::AddressMode::ClampToEdge,
367 address_mode_v: wgpu::AddressMode::ClampToEdge,
368 address_mode_w: wgpu::AddressMode::ClampToEdge,
369 mag_filter: wgpu::FilterMode::Linear,
370 min_filter: wgpu::FilterMode::Linear,
371 mipmap_filter: wgpu::MipmapFilterMode::Linear,
372 ..Default::default()
373 });
374 let video_plane_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
375 label: Some("Video Plane Sampler"),
376 address_mode_u: wgpu::AddressMode::ClampToEdge,
377 address_mode_v: wgpu::AddressMode::ClampToEdge,
378 address_mode_w: wgpu::AddressMode::ClampToEdge,
379 mag_filter: wgpu::FilterMode::Linear,
380 min_filter: wgpu::FilterMode::Linear,
381 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
382 ..Default::default()
383 });
384 let video_output_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
385 label: Some("Video Output Sampler"),
386 address_mode_u: wgpu::AddressMode::ClampToEdge,
387 address_mode_v: wgpu::AddressMode::ClampToEdge,
388 address_mode_w: wgpu::AddressMode::ClampToEdge,
389 mag_filter: wgpu::FilterMode::Linear,
390 min_filter: wgpu::FilterMode::Linear,
391 mipmap_filter: wgpu::MipmapFilterMode::Linear,
392 ..Default::default()
393 });
394
395 Ok(Self {
396 instance,
397 adapter,
398 device,
399 queue,
400 pipeline_layout,
401 shader,
402 bind_group_layout_tex,
403 bind_group_layout_uni,
404 pipeline: std::sync::Mutex::new(std::collections::HashMap::new()),
405 nv12_bind_group_layout,
406 nv12_pipeline,
407 static_sampler,
408 video_plane_sampler,
409 video_output_sampler,
410 })
411 }
412
413 pub fn validate_static_decode(
414 width: u32,
415 height: u32,
416 decoded_bytes: u64,
417 ) -> anyhow::Result<()> {
418 anyhow::ensure!(
419 width > 0 && height > 0,
420 "decoded image dimensions must be non-zero, got {width}x{height}"
421 );
422 anyhow::ensure!(
423 decoded_bytes <= MAX_STATIC_DECODE_BYTES,
424 "decoded image for {width}x{height} requires approximately {:.1} MiB, exceeding the {:.0} MiB safety limit",
425 decoded_bytes as f64 / (1024.0 * 1024.0),
426 MAX_STATIC_DECODE_BYTES as f64 / (1024.0 * 1024.0)
427 );
428 Ok(())
429 }
430
431 pub fn create_per_output_uniforms(&self) -> PerOutputUniforms {
434 let uniforms = Uniforms::default();
435 let buffer = self
436 .device
437 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
438 label: Some("Per-Output Uniform Buffer"),
439 contents: bytemuck::cast_slice(&[uniforms]),
440 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
441 });
442 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
443 layout: &self.bind_group_layout_uni,
444 entries: &[wgpu::BindGroupEntry {
445 binding: 0,
446 resource: buffer.as_entire_binding(),
447 }],
448 label: Some("per_output_uniform_bind_group"),
449 });
450 PerOutputUniforms { buffer, bind_group }
451 }
452
453 fn get_pipeline(&self, format: wgpu::TextureFormat) -> wgpu::RenderPipeline {
454 if let Ok(cache) = self.pipeline.lock().as_deref()
458 && let Some(pipeline) = cache.get(&format)
459 {
460 return pipeline.clone();
461 }
462
463 let pipeline = self
464 .device
465 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
466 label: Some("Render Pipeline"),
467 layout: Some(&self.pipeline_layout),
468 vertex: wgpu::VertexState {
469 module: &self.shader,
470 entry_point: Some("vs_main"),
471 buffers: &[],
472 compilation_options: wgpu::PipelineCompilationOptions::default(),
473 },
474 fragment: Some(wgpu::FragmentState {
475 module: &self.shader,
476 entry_point: Some("fs_main"),
477 targets: &[Some(wgpu::ColorTargetState {
478 format,
479 blend: Some(wgpu::BlendState::REPLACE),
480 write_mask: wgpu::ColorWrites::ALL,
481 })],
482 compilation_options: wgpu::PipelineCompilationOptions::default(),
483 }),
484 primitive: wgpu::PrimitiveState {
485 topology: wgpu::PrimitiveTopology::TriangleList,
486 strip_index_format: None,
487 front_face: wgpu::FrontFace::Ccw,
488 cull_mode: None, polygon_mode: wgpu::PolygonMode::Fill,
490 unclipped_depth: false,
491 conservative: false,
492 },
493 depth_stencil: None,
494 multisample: wgpu::MultisampleState {
495 count: 1,
496 mask: !0,
497 alpha_to_coverage_enabled: false,
498 },
499 multiview_mask: None,
500 cache: None,
501 });
502
503 if let Ok(mut cache) = self.pipeline.lock() {
506 cache.entry(format).or_insert_with(|| pipeline.clone());
507 if let Some(existing) = cache.get(&format) {
508 return existing.clone();
509 }
510 }
511 pipeline
512 }
513
514 pub fn create_texture(
515 &self,
516 width: u32,
517 height: u32,
518 ) -> anyhow::Result<(wgpu::Texture, wgpu::BindGroup)> {
519 validate_texture_dimensions(width, height, self.device.limits().max_texture_dimension_2d)?;
520 validate_texture_memory(width, height, 4)?;
521 let size = wgpu::Extent3d {
522 width,
523 height,
524 depth_or_array_layers: 1,
525 };
526
527 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
528 label: None,
529 size,
530 mip_level_count: 1,
531 sample_count: 1,
532 dimension: wgpu::TextureDimension::D2,
533 format: wgpu::TextureFormat::Rgba8UnormSrgb,
534 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
535 view_formats: &[],
536 });
537
538 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
539 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
540 layout: &self.bind_group_layout_tex,
541 entries: &[
542 wgpu::BindGroupEntry {
543 binding: 0,
544 resource: wgpu::BindingResource::TextureView(&view),
545 },
546 wgpu::BindGroupEntry {
547 binding: 1,
548 resource: wgpu::BindingResource::Sampler(&self.static_sampler),
549 },
550 ],
551 label: None,
552 });
553
554 Ok((texture, bind_group))
555 }
556
557 pub fn update_texture(&self, texture: &wgpu::Texture, rgba: &[u8], width: u32, height: u32) {
558 self.queue.write_texture(
559 wgpu::TexelCopyTextureInfo {
560 texture,
561 mip_level: 0,
562 origin: wgpu::Origin3d::ZERO,
563 aspect: wgpu::TextureAspect::All,
564 },
565 rgba,
566 wgpu::TexelCopyBufferLayout {
567 offset: 0,
568 bytes_per_row: Some(4 * width),
569 rows_per_image: Some(height),
570 },
571 wgpu::Extent3d {
572 width,
573 height,
574 depth_or_array_layers: 1,
575 },
576 );
577 }
578
579 pub fn create_video_texture(&self, width: u32, height: u32) -> anyhow::Result<VideoTexture> {
580 self.validate_video_texture(width, height)?;
581 let plane_texture = |label, size, format| {
582 self.device.create_texture(&wgpu::TextureDescriptor {
583 label: Some(label),
584 size,
585 mip_level_count: 1,
586 sample_count: 1,
587 dimension: wgpu::TextureDimension::D2,
588 format,
589 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
590 view_formats: &[],
591 })
592 };
593 let luma = plane_texture(
594 "Video NV12 Luma",
595 wgpu::Extent3d {
596 width,
597 height,
598 depth_or_array_layers: 1,
599 },
600 wgpu::TextureFormat::R8Unorm,
601 );
602 let chroma = plane_texture(
603 "Video NV12 Chroma",
604 wgpu::Extent3d {
605 width: width.div_ceil(2),
606 height: height.div_ceil(2),
607 depth_or_array_layers: 1,
608 },
609 wgpu::TextureFormat::Rg8Unorm,
610 );
611 let output = self.device.create_texture(&wgpu::TextureDescriptor {
612 label: Some("Video RGB Output"),
613 size: wgpu::Extent3d {
614 width,
615 height,
616 depth_or_array_layers: 1,
617 },
618 mip_level_count: 1,
619 sample_count: 1,
620 dimension: wgpu::TextureDimension::D2,
621 format: wgpu::TextureFormat::Rgba8UnormSrgb,
622 usage: wgpu::TextureUsages::TEXTURE_BINDING
623 | wgpu::TextureUsages::COPY_DST
624 | wgpu::TextureUsages::RENDER_ATTACHMENT,
625 view_formats: &[],
626 });
627 let conversion = YuvConversion::new(YuvColorInfo {
628 matrix: YuvMatrix::Bt709,
629 range: YuvRange::Limited,
630 });
631 let conversion_buffer = self
632 .device
633 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
634 label: Some("Video YUV Conversion Uniform"),
635 contents: bytemuck::bytes_of(&conversion),
636 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
637 });
638 let luma_view = luma.create_view(&wgpu::TextureViewDescriptor::default());
639 let chroma_view = chroma.create_view(&wgpu::TextureViewDescriptor::default());
640 let output_view = output.create_view(&wgpu::TextureViewDescriptor::default());
641 let conversion_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
642 label: Some("Video NV12 Conversion Bind Group"),
643 layout: &self.nv12_bind_group_layout,
644 entries: &[
645 wgpu::BindGroupEntry {
646 binding: 0,
647 resource: wgpu::BindingResource::TextureView(&luma_view),
648 },
649 wgpu::BindGroupEntry {
650 binding: 1,
651 resource: wgpu::BindingResource::TextureView(&chroma_view),
652 },
653 wgpu::BindGroupEntry {
654 binding: 2,
655 resource: wgpu::BindingResource::Sampler(&self.video_plane_sampler),
656 },
657 wgpu::BindGroupEntry {
658 binding: 3,
659 resource: conversion_buffer.as_entire_binding(),
660 },
661 ],
662 });
663 let effects_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
664 label: Some("Video Effects Bind Group"),
665 layout: &self.bind_group_layout_tex,
666 entries: &[
667 wgpu::BindGroupEntry {
668 binding: 0,
669 resource: wgpu::BindingResource::TextureView(&output_view),
670 },
671 wgpu::BindGroupEntry {
672 binding: 1,
673 resource: wgpu::BindingResource::Sampler(&self.video_output_sampler),
674 },
675 ],
676 });
677
678 Ok(VideoTexture {
679 output,
680 output_view,
681 effects_bind_group,
682 luma,
683 chroma,
684 conversion_buffer,
685 conversion_bind_group,
686 width,
687 height,
688 })
689 }
690
691 pub fn validate_video_texture(&self, width: u32, height: u32) -> anyhow::Result<()> {
692 validate_texture_dimensions(width, height, self.device.limits().max_texture_dimension_2d)?;
693 validate_texture_memory(width, height, 6)
696 }
697
698 pub fn update_video_texture(
699 &self,
700 texture: &VideoTexture,
701 frame: &VideoFrameData,
702 ) -> anyhow::Result<()> {
703 match frame {
704 VideoFrameData::Rgba(rgba) => {
705 anyhow::ensure!(
706 rgba.len() == texture.width as usize * texture.height as usize * 4,
707 "invalid RGBA video frame size"
708 );
709 self.update_texture(&texture.output, rgba, texture.width, texture.height);
710 }
711 VideoFrameData::Nv12 {
712 y_plane,
713 uv_plane,
714 color,
715 } => {
716 let chroma_width = texture.width.div_ceil(2);
717 let chroma_height = texture.height.div_ceil(2);
718 anyhow::ensure!(
719 y_plane.len() == texture.width as usize * texture.height as usize,
720 "invalid NV12 luma plane size"
721 );
722 anyhow::ensure!(
723 uv_plane.len() == (chroma_width * chroma_height * 2) as usize,
724 "invalid NV12 chroma plane size"
725 );
726 self.queue.write_texture(
727 texture.luma.as_image_copy(),
728 y_plane,
729 wgpu::TexelCopyBufferLayout {
730 offset: 0,
731 bytes_per_row: Some(texture.width),
732 rows_per_image: Some(texture.height),
733 },
734 wgpu::Extent3d {
735 width: texture.width,
736 height: texture.height,
737 depth_or_array_layers: 1,
738 },
739 );
740 self.queue.write_texture(
741 texture.chroma.as_image_copy(),
742 uv_plane,
743 wgpu::TexelCopyBufferLayout {
744 offset: 0,
745 bytes_per_row: Some(chroma_width * 2),
746 rows_per_image: Some(chroma_height),
747 },
748 wgpu::Extent3d {
749 width: chroma_width,
750 height: chroma_height,
751 depth_or_array_layers: 1,
752 },
753 );
754 self.queue.write_buffer(
755 &texture.conversion_buffer,
756 0,
757 bytemuck::bytes_of(&YuvConversion::new(*color)),
758 );
759
760 let mut encoder =
761 self.device
762 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
763 label: Some("NV12 Conversion Encoder"),
764 });
765 {
766 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
767 label: Some("NV12 Conversion Pass"),
768 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
769 view: &texture.output_view,
770 resolve_target: None,
771 depth_slice: None,
772 ops: wgpu::Operations {
773 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
774 store: wgpu::StoreOp::Store,
775 },
776 })],
777 depth_stencil_attachment: None,
778 occlusion_query_set: None,
779 timestamp_writes: None,
780 multiview_mask: None,
781 });
782 pass.set_pipeline(&self.nv12_pipeline);
783 pass.set_bind_group(0, &texture.conversion_bind_group, &[]);
784 pass.draw(0..3, 0..1);
785 }
786 self.queue.submit([encoder.finish()]);
787 }
788 }
789 Ok(())
790 }
791
792 pub fn load_texture(
793 &self,
794 image: &image::DynamicImage,
795 output_width: u32,
796 output_height: u32,
797 scaling_mode: u32,
798 ) -> anyhow::Result<(wgpu::Texture, wgpu::BindGroup, u32, u32)> {
799 let (rgba, width, height) =
803 self.prepare_image_rgba(image, output_width, output_height, scaling_mode)?;
804 let (texture, bind_group) = self.create_texture(width, height)?;
805 self.update_texture(&texture, &rgba, width, height);
806 Ok((texture, bind_group, width, height))
807 }
808
809 pub fn prepare_image_rgba(
813 &self,
814 image: &image::DynamicImage,
815 output_width: u32,
816 output_height: u32,
817 scaling_mode: u32,
818 ) -> anyhow::Result<(image::RgbaImage, u32, u32)> {
819 prepare_image_rgba(
820 image,
821 output_width,
822 output_height,
823 scaling_mode,
824 self.device.limits().max_texture_dimension_2d,
825 )
826 }
827}
828
829pub fn prepare_image_rgba(
834 image: &image::DynamicImage,
835 output_width: u32,
836 output_height: u32,
837 scaling_mode: u32,
838 max_texture_dimension: u32,
839) -> anyhow::Result<(image::RgbaImage, u32, u32)> {
840 let (width, height) = prepared_image_dimensions(
841 image.width(),
842 image.height(),
843 output_width,
844 output_height,
845 scaling_mode,
846 max_texture_dimension,
847 );
848 let rgba = if (width, height) == image.dimensions() {
849 image.to_rgba8()
850 } else {
851 let source = image.to_rgba8();
855 let source_view = fast_image_resize::images::ImageRef::new(
856 source.width(),
857 source.height(),
858 source.as_raw(),
859 fast_image_resize::PixelType::U8x4,
860 )?;
861 let mut destination = fast_image_resize::images::Image::new(
862 width,
863 height,
864 fast_image_resize::PixelType::U8x4,
865 );
866 let mut resizer = fast_image_resize::Resizer::new();
867 resizer.resize(&source_view, &mut destination, None)?;
868 image::RgbaImage::from_raw(width, height, destination.into_vec())
869 .ok_or_else(|| anyhow::anyhow!("resizer returned an invalid RGBA buffer"))?
870 };
871 Ok((rgba, width, height))
872}
873
874impl Renderer {
875 pub fn update_uniforms(&self, buffer: &wgpu::Buffer, uniforms: Uniforms) {
876 self.queue
877 .write_buffer(buffer, 0, bytemuck::cast_slice(&[uniforms]));
878 }
879
880 pub fn render_frame(
881 &self,
882 request: FrameRequest,
883 per_output: &PerOutputUniforms,
884 ) -> anyhow::Result<FrameStatus> {
885 let FrameRequest {
886 surface,
887 format,
888 bg_bind,
889 new_bind,
890 effect,
891 width,
892 height,
893 img_width,
894 img_height,
895 old_img_width,
896 old_img_height,
897 scaling_mode,
898 } = request;
899 let mut uniforms = Uniforms::from_effect(effect);
900 uniforms.resolution = [width as f32, height as f32];
901 uniforms.image_resolution = [img_width as f32, img_height as f32];
902 uniforms.old_image_resolution = [old_img_width as f32, old_img_height as f32];
903 uniforms.scaling_mode = scaling_mode;
904 self.update_uniforms(&per_output.buffer, uniforms);
905
906 let pipeline = self.get_pipeline(format);
907 let output = match surface.get_current_texture() {
912 wgpu::CurrentSurfaceTexture::Success(texture) => texture,
913 wgpu::CurrentSurfaceTexture::Suboptimal(texture) => texture,
914 wgpu::CurrentSurfaceTexture::Timeout => return Ok(FrameStatus::TimedOut),
915 wgpu::CurrentSurfaceTexture::Outdated => return Ok(FrameStatus::Outdated),
916 wgpu::CurrentSurfaceTexture::Lost => return Ok(FrameStatus::Lost),
917 wgpu::CurrentSurfaceTexture::Occluded => return Ok(FrameStatus::TimedOut),
918 wgpu::CurrentSurfaceTexture::Validation => {
919 return Err(anyhow::anyhow!(
920 "failed to acquire swapchain texture: validation error"
921 ));
922 }
923 };
924 let view = output
925 .texture
926 .create_view(&wgpu::TextureViewDescriptor::default());
927
928 let mut encoder = self
929 .device
930 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
931 label: Some("Render Encoder"),
932 });
933
934 {
935 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
936 label: Some("Wallpaper Render Pass"),
937 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
938 view: &view,
939 resolve_target: None,
940 depth_slice: None,
941 ops: wgpu::Operations {
942 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
943 store: wgpu::StoreOp::Store,
944 },
945 })],
946 depth_stencil_attachment: None,
947 occlusion_query_set: None,
948 timestamp_writes: None,
949 multiview_mask: None,
950 });
951
952 render_pass.set_pipeline(&pipeline);
953 render_pass.set_bind_group(0, bg_bind, &[]);
954 render_pass.set_bind_group(1, new_bind, &[]);
955 render_pass.set_bind_group(2, &per_output.bind_group, &[]);
956 render_pass.draw(0..3, 0..1);
958 }
959
960 self.queue.submit(std::iter::once(encoder.finish()));
961 self.queue.present(output);
962
963 Ok(FrameStatus::Presented)
964 }
965}
966
967#[derive(Debug, Clone, Copy, PartialEq, Eq)]
969pub enum FrameStatus {
970 Presented,
971 TimedOut,
972 Outdated,
973 Lost,
974}
975
976fn validate_texture_dimensions(width: u32, height: u32, limit: u32) -> anyhow::Result<()> {
977 anyhow::ensure!(
978 width > 0 && height > 0,
979 "texture dimensions must be non-zero, got {width}x{height}"
980 );
981 anyhow::ensure!(
982 width <= limit && height <= limit,
983 "texture dimensions {width}x{height} exceed the GPU limit of {limit}"
984 );
985 Ok(())
986}
987
988fn validate_texture_memory(width: u32, height: u32, bytes_per_pixel: u64) -> anyhow::Result<()> {
989 validate_image_memory(
990 width,
991 height,
992 bytes_per_pixel,
993 MAX_TEXTURE_BYTES,
994 "texture allocation",
995 )
996}
997
998fn validate_image_memory(
999 width: u32,
1000 height: u32,
1001 bytes_per_pixel: u64,
1002 limit: u64,
1003 label: &str,
1004) -> anyhow::Result<()> {
1005 let bytes = u64::from(width)
1006 .checked_mul(u64::from(height))
1007 .and_then(|pixels| pixels.checked_mul(bytes_per_pixel))
1008 .ok_or_else(|| anyhow::anyhow!("{label} size overflow for {width}x{height}"))?;
1009 anyhow::ensure!(
1010 bytes <= limit,
1011 "{label} for {width}x{height} requires approximately {:.1} MiB, exceeding the {:.0} MiB safety limit",
1012 bytes as f64 / (1024.0 * 1024.0),
1013 limit as f64 / (1024.0 * 1024.0)
1014 );
1015 Ok(())
1016}
1017
1018fn prepared_image_dimensions(
1019 image_width: u32,
1020 image_height: u32,
1021 output_width: u32,
1022 output_height: u32,
1023 scaling_mode: u32,
1024 texture_limit: u32,
1025) -> (u32, u32) {
1026 if image_width == 0 || image_height == 0 {
1027 return (image_width, image_height);
1028 }
1029
1030 let limit_scale = (texture_limit as f64 / image_width as f64)
1031 .min(texture_limit as f64 / image_height as f64)
1032 .min(1.0);
1033 let output_scale = match scaling_mode {
1034 0 if output_width > 0 && output_height > 0 => (output_width as f64 / image_width as f64)
1037 .max(output_height as f64 / image_height as f64)
1038 .min(1.0),
1039 1 if output_width > 0 && output_height > 0 => (output_width as f64 / image_width as f64)
1040 .min(output_height as f64 / image_height as f64)
1041 .min(1.0),
1042 2 if output_width > 0 && output_height > 0 => {
1043 return (
1044 image_width.min(output_width).min(texture_limit).max(1),
1045 image_height.min(output_height).min(texture_limit).max(1),
1046 );
1047 }
1048 _ => return (image_width, image_height),
1051 };
1052 let scale = limit_scale.min(output_scale);
1053
1054 (
1055 ((image_width as f64 * scale).round() as u32).max(1),
1056 ((image_height as f64 * scale).round() as u32).max(1),
1057 )
1058}
1059
1060pub struct FrameRequest<'a> {
1062 pub surface: &'a wgpu::Surface<'a>,
1063 pub format: wgpu::TextureFormat,
1064 pub bg_bind: &'a wgpu::BindGroup,
1066 pub new_bind: &'a wgpu::BindGroup,
1068 pub effect: &'a crate::animation::EffectUniforms,
1070 pub width: u32,
1071 pub height: u32,
1072 pub img_width: u32,
1073 pub img_height: u32,
1074 pub old_img_width: u32,
1075 pub old_img_height: u32,
1076 pub scaling_mode: u32,
1078}
1079
1080#[cfg(test)]
1081mod tests {
1082 use super::*;
1083
1084 #[test]
1090 fn transition_renders_mid_fade_blend_offscreen() {
1091 let runtime = match tokio::runtime::Builder::new_current_thread()
1092 .enable_all()
1093 .build()
1094 {
1095 Ok(runtime) => runtime,
1096 Err(_) => return,
1097 };
1098 let renderer = match runtime.block_on(Renderer::new(&GpuSelection::Auto)) {
1099 Ok(renderer) => renderer,
1100 Err(_) => {
1101 eprintln!("SKIP: no GPU adapter for offscreen transition test");
1102 return;
1103 }
1104 };
1105 const SIZE: u32 = 64;
1106 let red = [255u8, 0, 0, 255].repeat((SIZE * SIZE) as usize);
1107 let blue = [0u8, 0, 255, 255].repeat((SIZE * SIZE) as usize);
1108 let (tex_old, bind_old) = renderer.create_texture(SIZE, SIZE).expect("old texture");
1109 let (tex_new, bind_new) = renderer.create_texture(SIZE, SIZE).expect("new texture");
1110 renderer.update_texture(&tex_old, &red, SIZE, SIZE);
1111 renderer.update_texture(&tex_new, &blue, SIZE, SIZE);
1112
1113 let per_output = renderer.create_per_output_uniforms();
1114 let render_at = |effect: &crate::animation::Effect, progress: f32| -> Vec<u8> {
1115 let effect = crate::animation::compute_effect_uniforms(effect, progress);
1116 let mut uniforms = Uniforms::from_effect(&effect);
1117 uniforms.resolution = [SIZE as f32, SIZE as f32];
1118 uniforms.image_resolution = [SIZE as f32, SIZE as f32];
1119 uniforms.old_image_resolution = [SIZE as f32, SIZE as f32];
1120 uniforms.scaling_mode = 0;
1121 renderer.update_uniforms(&per_output.buffer, uniforms);
1122
1123 let target = renderer.device.create_texture(&wgpu::TextureDescriptor {
1124 label: Some("offscreen transition target"),
1125 size: wgpu::Extent3d {
1126 width: SIZE,
1127 height: SIZE,
1128 depth_or_array_layers: 1,
1129 },
1130 mip_level_count: 1,
1131 sample_count: 1,
1132 dimension: wgpu::TextureDimension::D2,
1133 format: wgpu::TextureFormat::Rgba8UnormSrgb,
1134 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1135 view_formats: &[],
1136 });
1137 let view = target.create_view(&wgpu::TextureViewDescriptor::default());
1138 let pipeline = renderer.get_pipeline(wgpu::TextureFormat::Rgba8UnormSrgb);
1139 let mut encoder = renderer
1140 .device
1141 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
1142 {
1143 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1144 label: Some("offscreen transition"),
1145 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1146 view: &view,
1147 resolve_target: None,
1148 depth_slice: None,
1149 ops: wgpu::Operations {
1150 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1151 store: wgpu::StoreOp::Store,
1152 },
1153 })],
1154 depth_stencil_attachment: None,
1155 occlusion_query_set: None,
1156 timestamp_writes: None,
1157 multiview_mask: None,
1158 });
1159 pass.set_pipeline(&pipeline);
1160 pass.set_bind_group(0, &bind_old, &[]);
1161 pass.set_bind_group(1, &bind_new, &[]);
1162 pass.set_bind_group(2, &per_output.bind_group, &[]);
1163 pass.draw(0..3, 0..1);
1164 }
1165 let readback = renderer.device.create_buffer(&wgpu::BufferDescriptor {
1166 label: Some("offscreen readback"),
1167 size: (SIZE * SIZE * 4) as u64,
1168 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1169 mapped_at_creation: false,
1170 });
1171 encoder.copy_texture_to_buffer(
1172 wgpu::TexelCopyTextureInfo {
1173 texture: &target,
1174 mip_level: 0,
1175 origin: wgpu::Origin3d::ZERO,
1176 aspect: wgpu::TextureAspect::All,
1177 },
1178 wgpu::TexelCopyBufferInfo {
1179 buffer: &readback,
1180 layout: wgpu::TexelCopyBufferLayout {
1181 offset: 0,
1182 bytes_per_row: Some(SIZE * 4),
1183 rows_per_image: Some(SIZE),
1184 },
1185 },
1186 wgpu::Extent3d {
1187 width: SIZE,
1188 height: SIZE,
1189 depth_or_array_layers: 1,
1190 },
1191 );
1192 renderer.queue.submit([encoder.finish()]);
1193 let slice = readback.slice(..);
1194 slice.map_async(wgpu::MapMode::Read, |_| {});
1195 renderer
1196 .device
1197 .poll(wgpu::PollType::wait_indefinitely())
1198 .ok();
1199 let pixels = {
1200 let mapped = slice.get_mapped_range().unwrap();
1201 mapped.to_vec()
1202 };
1203 readback.unmap();
1204 pixels
1205 };
1206 let pixel_at = |frame: &[u8], x: u32, y: u32| -> [u8; 4] {
1207 let i = ((y * SIZE + x) * 4) as usize;
1208 [frame[i], frame[i + 1], frame[i + 2], frame[i + 3]]
1209 };
1210
1211 let fade = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1212 let start = render_at(&fade, 0.0);
1214 let start = pixel_at(&start, SIZE / 2, SIZE / 2);
1215 assert!(start[0] > 240 && start[2] < 15, "start pixel: {start:?}");
1216 let end = render_at(&fade, 1.0);
1217 let end = pixel_at(&end, SIZE / 2, SIZE / 2);
1218 assert!(end[2] > 240 && end[0] < 15, "end pixel: {end:?}");
1219 let mid = render_at(&fade, 0.5);
1224 let mid = pixel_at(&mid, SIZE / 2, SIZE / 2);
1225 assert!(
1226 (160..=215).contains(&mid[0])
1227 && (160..=215).contains(&mid[2])
1228 && mid[2] > mid[0]
1229 && mid[1] < 20,
1230 "mid pixel: {mid:?}"
1231 );
1232
1233 let wipe = crate::animation::Effect::Wipe(crate::animation::WipeParams::default());
1237 let frame = render_at(&wipe, 0.5);
1238 let left = pixel_at(&frame, 4, SIZE / 2);
1239 assert!(
1240 left[2] > 240 && left[0] < 15,
1241 "wipe revealed pixel: {left:?}"
1242 );
1243 let right = pixel_at(&frame, SIZE - 5, SIZE / 2);
1244 assert!(
1245 right[0] > 240 && right[2] < 15,
1246 "wipe unrevealed pixel: {right:?}"
1247 );
1248
1249 let at = |direction: crate::animation::WipeDirection, progress, x, y| {
1253 let e = crate::animation::Effect::Wipe(crate::animation::WipeParams {
1254 direction,
1255 ..crate::animation::WipeParams::default()
1256 });
1257 let f = render_at(&e, progress);
1258 pixel_at(&f, x, y)
1259 };
1260 let top = at(crate::animation::WipeDirection::Up, 1.0, SIZE / 2, 2);
1261 assert!(top[2] > 240 && top[0] < 15, "up wipe end pixel: {top:?}");
1262 let bottom = at(
1263 crate::animation::WipeDirection::Down,
1264 1.0,
1265 SIZE / 2,
1266 SIZE - 2,
1267 );
1268 assert!(
1269 bottom[2] > 240 && bottom[0] < 15,
1270 "down wipe end pixel: {bottom:?}"
1271 );
1272 let mid_top = at(
1273 crate::animation::WipeDirection::Down,
1274 0.5,
1275 SIZE / 2,
1276 SIZE - 2,
1277 );
1278 assert!(
1279 mid_top[2] > 240 && mid_top[0] < 15,
1280 "down wipe reveals bottom first: {mid_top:?}"
1281 );
1282 let far_top = at(crate::animation::WipeDirection::Down, 0.5, SIZE / 2, 2);
1283 assert!(
1284 far_top[0] > 240 && far_top[2] < 15,
1285 "down wipe keeps top old: {far_top:?}"
1286 );
1287
1288 let mid_col = SIZE / 2;
1293 let sweep = [
1294 at(
1295 crate::animation::WipeDirection::Left,
1296 0.2,
1297 mid_col,
1298 SIZE / 2,
1299 ),
1300 at(
1301 crate::animation::WipeDirection::Left,
1302 0.4,
1303 mid_col,
1304 SIZE / 2,
1305 ),
1306 at(
1307 crate::animation::WipeDirection::Left,
1308 0.6,
1309 mid_col,
1310 SIZE / 2,
1311 ),
1312 at(
1313 crate::animation::WipeDirection::Left,
1314 0.8,
1315 mid_col,
1316 SIZE / 2,
1317 ),
1318 ];
1319 assert!(
1320 sweep[0][0] > 240 && sweep[0][2] < 15,
1321 "early old: {:?}",
1322 sweep[0]
1323 );
1324 assert!(
1325 sweep[1][0] > 240 && sweep[1][2] < 15,
1326 "early old: {:?}",
1327 sweep[1]
1328 );
1329 assert!(
1330 sweep[2][2] > 240 && sweep[2][0] < 15,
1331 "late new: {:?}",
1332 sweep[2]
1333 );
1334 assert!(
1335 sweep[3][2] > 240 && sweep[3][0] < 15,
1336 "late new: {:?}",
1337 sweep[3]
1338 );
1339 let right_entry = at(
1341 crate::animation::WipeDirection::Right,
1342 0.5,
1343 SIZE - 2,
1344 SIZE / 2,
1345 );
1346 assert!(
1347 right_entry[2] > 240 && right_entry[0] < 15,
1348 "right wipe reveals right first: {right_entry:?}"
1349 );
1350 let up_entry = at(crate::animation::WipeDirection::Up, 0.5, SIZE / 2, 2);
1351 assert!(
1352 up_entry[2] > 240 && up_entry[0] < 15,
1353 "up wipe reveals top first: {up_entry:?}"
1354 );
1355
1356 let grow = crate::animation::Effect::Grow(crate::animation::GrowParams::default());
1359 let frame = render_at(&grow, 0.5);
1360 let center = pixel_at(&frame, SIZE / 2, SIZE / 2);
1361 assert!(
1362 center[2] > 240 && center[0] < 15,
1363 "grow revealed pixel: {center:?}"
1364 );
1365 let corner = pixel_at(&frame, 2, 2);
1366 assert!(
1367 corner[0] > 240 && corner[2] < 15,
1368 "grow unrevealed pixel: {corner:?}"
1369 );
1370 }
1371
1372 #[test]
1373 fn selects_yuv_conversion_coefficients_and_range() {
1374 let limited_709 = YuvConversion::new(YuvColorInfo {
1375 matrix: YuvMatrix::Bt709,
1376 range: YuvRange::Limited,
1377 });
1378 assert_eq!(limited_709.red, [1.0, 0.0, 1.5748, 0.0]);
1379 assert_eq!(limited_709.green, [1.0, -0.187_324, -0.468_124, 0.0]);
1380 assert_eq!(limited_709.range[0], 16.0 / 255.0);
1381 assert_eq!(limited_709.range[1], 255.0 / 219.0);
1382
1383 let full_2020 = YuvConversion::new(YuvColorInfo {
1384 matrix: YuvMatrix::Bt2020,
1385 range: YuvRange::Full,
1386 });
1387 assert_eq!(full_2020.red, [1.0, 0.0, 1.4746, 0.0]);
1388 assert_eq!(full_2020.blue, [1.0, 1.8814, 0.0, 0.0]);
1389 assert_eq!(full_2020.range, [0.0, 1.0, 128.0 / 255.0, 1.0]);
1390 }
1391
1392 #[test]
1393 fn validates_texture_dimensions_before_wgpu() {
1394 assert!(validate_texture_dimensions(8192, 8192, 8192).is_ok());
1395 assert!(validate_texture_dimensions(0, 1080, 8192).is_err());
1396 assert!(validate_texture_dimensions(8193, 1080, 8192).is_err());
1397 assert!(validate_texture_dimensions(1920, 8193, 8192).is_err());
1398 assert!(validate_texture_memory(7_680, 4_320, 6).is_ok());
1399 assert!(validate_texture_memory(16_384, 16_384, 4).is_err());
1400 assert!(Renderer::validate_static_decode(11_322, 6_192, 210_304_512).is_ok());
1401 assert!(Renderer::validate_static_decode(32_768, 32_768, u64::MAX).is_err());
1402 }
1403
1404 #[test]
1405 fn fill_reduces_large_images_to_cover_the_output() {
1406 assert_eq!(
1407 prepared_image_dimensions(11_322, 6_192, 3_840, 2_160, 0, 32_768),
1408 (3_950, 2_160)
1409 );
1410 }
1411
1412 #[test]
1413 fn fit_preserves_aspect_ratio_without_upscaling() {
1414 assert_eq!(
1415 prepared_image_dimensions(11_322, 6_192, 3_840, 2_160, 1, 32_768),
1416 (3_840, 2_100)
1417 );
1418 assert_eq!(
1419 prepared_image_dimensions(1_920, 1_080, 3_840, 2_160, 1, 32_768),
1420 (1_920, 1_080)
1421 );
1422 }
1423
1424 #[test]
1425 fn pixel_sensitive_modes_preserve_native_dimensions() {
1426 assert_eq!(
1427 prepared_image_dimensions(11_322, 6_192, 3_840, 2_160, 3, 8_192),
1428 (11_322, 6_192)
1429 );
1430 assert_eq!(
1431 prepared_image_dimensions(3_840, 2_160, 2_560, 1_440, 4, 8_192),
1432 (3_840, 2_160)
1433 );
1434 }
1435
1436 #[test]
1437 fn stretch_does_not_upscale_small_images() {
1438 assert_eq!(
1439 prepared_image_dimensions(1, 1, 3_840, 2_160, 2, 32_768),
1440 (1, 1)
1441 );
1442 assert_eq!(
1443 prepared_image_dimensions(5_000, 1_000, 3_840, 2_160, 2, 32_768),
1444 (3_840, 1_000)
1445 );
1446 }
1447}