1use image::GenericImageView;
2use wgpu::util::DeviceExt;
3
4use crate::video::{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: std::sync::Mutex<Option<(wgpu::TextureFormat, wgpu::RenderPipeline)>>,
20 nv12_bind_group_layout: wgpu::BindGroupLayout,
21 nv12_pipeline: wgpu::RenderPipeline,
22}
23
24pub struct VideoTexture {
25 output: wgpu::Texture,
26 output_view: wgpu::TextureView,
27 effects_bind_group: wgpu::BindGroup,
28 luma: wgpu::Texture,
29 chroma: wgpu::Texture,
30 conversion_buffer: wgpu::Buffer,
31 conversion_bind_group: wgpu::BindGroup,
32 width: u32,
33 height: u32,
34}
35
36impl VideoTexture {
37 pub fn texture(&self) -> &wgpu::Texture {
38 &self.output
39 }
40
41 pub fn bind_group(&self) -> &wgpu::BindGroup {
42 &self.effects_bind_group
43 }
44}
45
46#[repr(C)]
47#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
48struct YuvConversion {
49 range: [f32; 4],
50 red: [f32; 4],
51 green: [f32; 4],
52 blue: [f32; 4],
53}
54
55impl YuvConversion {
56 fn new(color: YuvColorInfo) -> Self {
57 let range = match color.range {
58 YuvRange::Limited => [16.0 / 255.0, 255.0 / 219.0, 128.0 / 255.0, 255.0 / 224.0],
59 YuvRange::Full => [0.0, 1.0, 128.0 / 255.0, 1.0],
60 };
61 let (red_cr, green_cb, green_cr, blue_cb) = match color.matrix {
62 YuvMatrix::Bt601 => (1.402, -0.344_136, -0.714_136, 1.772),
63 YuvMatrix::Bt709 => (1.5748, -0.187_324, -0.468_124, 1.8556),
64 YuvMatrix::Bt2020 => (1.4746, -0.164_553, -0.571_353, 1.8814),
65 };
66 Self {
67 range,
68 red: [1.0, 0.0, red_cr, 0.0],
69 green: [1.0, green_cb, green_cr, 0.0],
70 blue: [1.0, blue_cb, 0.0, 0.0],
71 }
72 }
73}
74
75pub struct PerOutputUniforms {
78 pub buffer: wgpu::Buffer,
79 pub bind_group: wgpu::BindGroup,
80}
81
82#[repr(C)]
83#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
84pub struct Uniforms {
85 pub time: f32,
86 pub progress: f32,
87 pub effect_type: u32,
88 pub padding: u32,
89 pub resolution: [f32; 2],
90 pub image_resolution: [f32; 2],
91 pub old_image_resolution: [f32; 2],
92 pub param_a: f32,
93 pub param_b: f32,
94 pub param_c: f32,
95 pub param_d: f32,
96 pub origin: [f32; 2],
97 pub direction: [f32; 2],
98 pub easing: u32,
99 pub scaling_mode: u32,
100}
101
102impl Uniforms {
103 pub fn from_effect(effect: &crate::animation::EffectUniforms) -> Self {
104 Self {
105 time: effect.progress,
106 progress: effect.progress,
107 effect_type: effect.effect_type,
108 padding: 0,
109 resolution: [1920.0, 1080.0],
110 image_resolution: [1920.0, 1080.0],
111 old_image_resolution: [1920.0, 1080.0],
112 param_a: effect.param_a,
113 param_b: effect.param_b,
114 param_c: effect.param_c,
115 param_d: effect.param_d,
116 origin: effect.origin,
117 direction: effect.direction,
118 easing: effect.easing,
119 scaling_mode: 0,
120 }
121 }
122}
123
124impl Default for Uniforms {
125 fn default() -> Self {
126 Self {
127 time: 0.0,
128 progress: 0.0,
129 effect_type: 0,
130 padding: 0,
131 resolution: [1920.0, 1080.0],
132 image_resolution: [1920.0, 1080.0],
133 old_image_resolution: [1920.0, 1080.0],
134 param_a: 0.0,
135 param_b: 0.0,
136 param_c: 0.0,
137 param_d: 0.0,
138 origin: [0.5, 0.5],
139 direction: [0.0, 0.0],
140 easing: 3,
141 scaling_mode: 0,
142 }
143 }
144}
145
146impl Renderer {
147 pub async fn new() -> anyhow::Result<Self> {
148 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
149 backends: wgpu::Backends::all(),
150 ..Default::default()
151 });
152
153 let adapter = instance
154 .request_adapter(&wgpu::RequestAdapterOptions {
155 power_preference: wgpu::PowerPreference::HighPerformance,
156 compatible_surface: None,
157 force_fallback_adapter: false,
158 })
159 .await
160 .ok_or_else(|| anyhow::anyhow!("Failed to find suitable adapter"))?;
161
162 let adapter_limits = adapter.limits();
163 let required_limits = wgpu::Limits {
164 max_texture_dimension_2d: adapter_limits.max_texture_dimension_2d,
165 ..wgpu::Limits::default()
166 };
167
168 let (device, queue) = adapter
169 .request_device(
170 &wgpu::DeviceDescriptor {
171 label: None,
172 required_features: wgpu::Features::empty(),
173 required_limits,
174 memory_hints: wgpu::MemoryHints::default(),
175 },
176 None,
177 )
178 .await?;
179
180 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
181 label: Some("Effects Shader"),
182 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
183 crate::shader::EFFECTS_SHADER,
184 )),
185 });
186
187 let bind_group_layout_tex =
188 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
189 entries: &[
190 wgpu::BindGroupLayoutEntry {
191 binding: 0,
192 visibility: wgpu::ShaderStages::FRAGMENT,
193 ty: wgpu::BindingType::Texture {
194 multisampled: false,
195 view_dimension: wgpu::TextureViewDimension::D2,
196 sample_type: wgpu::TextureSampleType::Float { filterable: true },
197 },
198 count: None,
199 },
200 wgpu::BindGroupLayoutEntry {
201 binding: 1,
202 visibility: wgpu::ShaderStages::FRAGMENT,
203 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
204 count: None,
205 },
206 ],
207 label: Some("texture_bind_group_layout"),
208 });
209
210 let bind_group_layout_uni =
211 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
212 entries: &[wgpu::BindGroupLayoutEntry {
213 binding: 0,
214 visibility: wgpu::ShaderStages::FRAGMENT,
215 ty: wgpu::BindingType::Buffer {
216 ty: wgpu::BufferBindingType::Uniform,
217 has_dynamic_offset: false,
218 min_binding_size: None,
219 },
220 count: None,
221 }],
222 label: Some("uniform_bind_group_layout"),
223 });
224
225 let nv12_bind_group_layout =
226 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
227 label: Some("NV12 Conversion Bind Group Layout"),
228 entries: &[
229 wgpu::BindGroupLayoutEntry {
230 binding: 0,
231 visibility: wgpu::ShaderStages::FRAGMENT,
232 ty: wgpu::BindingType::Texture {
233 multisampled: false,
234 view_dimension: wgpu::TextureViewDimension::D2,
235 sample_type: wgpu::TextureSampleType::Float { filterable: true },
236 },
237 count: None,
238 },
239 wgpu::BindGroupLayoutEntry {
240 binding: 1,
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: 2,
251 visibility: wgpu::ShaderStages::FRAGMENT,
252 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
253 count: None,
254 },
255 wgpu::BindGroupLayoutEntry {
256 binding: 3,
257 visibility: wgpu::ShaderStages::FRAGMENT,
258 ty: wgpu::BindingType::Buffer {
259 ty: wgpu::BufferBindingType::Uniform,
260 has_dynamic_offset: false,
261 min_binding_size: None,
262 },
263 count: None,
264 },
265 ],
266 });
267
268 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
269 label: Some("Render Pipeline Layout"),
270 bind_group_layouts: &[
271 &bind_group_layout_tex,
272 &bind_group_layout_tex,
273 &bind_group_layout_uni,
274 ],
275 push_constant_ranges: &[],
276 });
277
278 let nv12_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
279 label: Some("NV12 to RGB Shader"),
280 source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
281 crate::shader::NV12_TO_RGB_SHADER,
282 )),
283 });
284 let nv12_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
285 label: Some("NV12 Conversion Pipeline Layout"),
286 bind_group_layouts: &[&nv12_bind_group_layout],
287 push_constant_ranges: &[],
288 });
289 let nv12_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
290 label: Some("NV12 Conversion Pipeline"),
291 layout: Some(&nv12_pipeline_layout),
292 vertex: wgpu::VertexState {
293 module: &nv12_shader,
294 entry_point: Some("vs_main"),
295 buffers: &[],
296 compilation_options: wgpu::PipelineCompilationOptions::default(),
297 },
298 fragment: Some(wgpu::FragmentState {
299 module: &nv12_shader,
300 entry_point: Some("fs_main"),
301 targets: &[Some(wgpu::ColorTargetState {
302 format: wgpu::TextureFormat::Rgba8UnormSrgb,
303 blend: Some(wgpu::BlendState::REPLACE),
304 write_mask: wgpu::ColorWrites::ALL,
305 })],
306 compilation_options: wgpu::PipelineCompilationOptions::default(),
307 }),
308 primitive: wgpu::PrimitiveState::default(),
309 depth_stencil: None,
310 multisample: wgpu::MultisampleState::default(),
311 multiview: None,
312 cache: None,
313 });
314
315 Ok(Self {
316 instance,
317 adapter,
318 device,
319 queue,
320 pipeline_layout,
321 shader,
322 bind_group_layout_tex,
323 bind_group_layout_uni,
324 pipeline: std::sync::Mutex::new(None),
325 nv12_bind_group_layout,
326 nv12_pipeline,
327 })
328 }
329
330 pub fn validate_static_decode(
331 width: u32,
332 height: u32,
333 decoded_bytes: u64,
334 ) -> anyhow::Result<()> {
335 anyhow::ensure!(
336 width > 0 && height > 0,
337 "decoded image dimensions must be non-zero, got {width}x{height}"
338 );
339 anyhow::ensure!(
340 decoded_bytes <= MAX_STATIC_DECODE_BYTES,
341 "decoded image for {width}x{height} requires approximately {:.1} MiB, exceeding the {:.0} MiB safety limit",
342 decoded_bytes as f64 / (1024.0 * 1024.0),
343 MAX_STATIC_DECODE_BYTES as f64 / (1024.0 * 1024.0)
344 );
345 Ok(())
346 }
347
348 pub fn create_per_output_uniforms(&self) -> PerOutputUniforms {
351 let uniforms = Uniforms::default();
352 let buffer = self
353 .device
354 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
355 label: Some("Per-Output Uniform Buffer"),
356 contents: bytemuck::cast_slice(&[uniforms]),
357 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
358 });
359 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
360 layout: &self.bind_group_layout_uni,
361 entries: &[wgpu::BindGroupEntry {
362 binding: 0,
363 resource: buffer.as_entire_binding(),
364 }],
365 label: Some("per_output_uniform_bind_group"),
366 });
367 PerOutputUniforms { buffer, bind_group }
368 }
369
370 fn get_pipeline(&self, format: wgpu::TextureFormat) -> wgpu::RenderPipeline {
371 let mut cache = self.pipeline.lock().unwrap();
372 if let Some((cached_fmt, ref pipeline)) = *cache
373 && cached_fmt == format
374 {
375 return pipeline.clone();
376 }
377
378 let pipeline = self
379 .device
380 .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
381 label: Some("Render Pipeline"),
382 layout: Some(&self.pipeline_layout),
383 vertex: wgpu::VertexState {
384 module: &self.shader,
385 entry_point: Some("vs_main"),
386 buffers: &[],
387 compilation_options: wgpu::PipelineCompilationOptions::default(),
388 },
389 fragment: Some(wgpu::FragmentState {
390 module: &self.shader,
391 entry_point: Some("fs_main"),
392 targets: &[Some(wgpu::ColorTargetState {
393 format,
394 blend: Some(wgpu::BlendState::REPLACE),
395 write_mask: wgpu::ColorWrites::ALL,
396 })],
397 compilation_options: wgpu::PipelineCompilationOptions::default(),
398 }),
399 primitive: wgpu::PrimitiveState {
400 topology: wgpu::PrimitiveTopology::TriangleList,
401 strip_index_format: None,
402 front_face: wgpu::FrontFace::Ccw,
403 cull_mode: None, polygon_mode: wgpu::PolygonMode::Fill,
405 unclipped_depth: false,
406 conservative: false,
407 },
408 depth_stencil: None,
409 multisample: wgpu::MultisampleState {
410 count: 1,
411 mask: !0,
412 alpha_to_coverage_enabled: false,
413 },
414 multiview: None,
415 cache: None,
416 });
417
418 *cache = Some((format, pipeline.clone()));
419 pipeline
420 }
421
422 pub fn create_texture(
423 &self,
424 width: u32,
425 height: u32,
426 ) -> anyhow::Result<(wgpu::Texture, wgpu::BindGroup)> {
427 validate_texture_dimensions(width, height, self.device.limits().max_texture_dimension_2d)?;
428 validate_texture_memory(width, height, 4)?;
429 let size = wgpu::Extent3d {
430 width,
431 height,
432 depth_or_array_layers: 1,
433 };
434
435 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
436 label: None,
437 size,
438 mip_level_count: 1,
439 sample_count: 1,
440 dimension: wgpu::TextureDimension::D2,
441 format: wgpu::TextureFormat::Rgba8UnormSrgb,
442 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
443 view_formats: &[],
444 });
445
446 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
447 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
448 address_mode_u: wgpu::AddressMode::ClampToEdge,
449 address_mode_v: wgpu::AddressMode::ClampToEdge,
450 address_mode_w: wgpu::AddressMode::ClampToEdge,
451 mag_filter: wgpu::FilterMode::Linear,
452 min_filter: wgpu::FilterMode::Nearest,
453 mipmap_filter: wgpu::FilterMode::Nearest,
454 ..Default::default()
455 });
456 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
457 layout: &self.bind_group_layout_tex,
458 entries: &[
459 wgpu::BindGroupEntry {
460 binding: 0,
461 resource: wgpu::BindingResource::TextureView(&view),
462 },
463 wgpu::BindGroupEntry {
464 binding: 1,
465 resource: wgpu::BindingResource::Sampler(&sampler),
466 },
467 ],
468 label: None,
469 });
470
471 Ok((texture, bind_group))
472 }
473
474 pub fn update_texture(&self, texture: &wgpu::Texture, rgba: &[u8], width: u32, height: u32) {
475 self.queue.write_texture(
476 wgpu::TexelCopyTextureInfo {
477 texture,
478 mip_level: 0,
479 origin: wgpu::Origin3d::ZERO,
480 aspect: wgpu::TextureAspect::All,
481 },
482 rgba,
483 wgpu::TexelCopyBufferLayout {
484 offset: 0,
485 bytes_per_row: Some(4 * width),
486 rows_per_image: Some(height),
487 },
488 wgpu::Extent3d {
489 width,
490 height,
491 depth_or_array_layers: 1,
492 },
493 );
494 }
495
496 pub fn create_video_texture(&self, width: u32, height: u32) -> anyhow::Result<VideoTexture> {
497 self.validate_video_texture(width, height)?;
498 let plane_texture = |label, size, format| {
499 self.device.create_texture(&wgpu::TextureDescriptor {
500 label: Some(label),
501 size,
502 mip_level_count: 1,
503 sample_count: 1,
504 dimension: wgpu::TextureDimension::D2,
505 format,
506 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
507 view_formats: &[],
508 })
509 };
510 let luma = plane_texture(
511 "Video NV12 Luma",
512 wgpu::Extent3d {
513 width,
514 height,
515 depth_or_array_layers: 1,
516 },
517 wgpu::TextureFormat::R8Unorm,
518 );
519 let chroma = plane_texture(
520 "Video NV12 Chroma",
521 wgpu::Extent3d {
522 width: width.div_ceil(2),
523 height: height.div_ceil(2),
524 depth_or_array_layers: 1,
525 },
526 wgpu::TextureFormat::Rg8Unorm,
527 );
528 let output = self.device.create_texture(&wgpu::TextureDescriptor {
529 label: Some("Video RGB Output"),
530 size: wgpu::Extent3d {
531 width,
532 height,
533 depth_or_array_layers: 1,
534 },
535 mip_level_count: 1,
536 sample_count: 1,
537 dimension: wgpu::TextureDimension::D2,
538 format: wgpu::TextureFormat::Rgba8UnormSrgb,
539 usage: wgpu::TextureUsages::TEXTURE_BINDING
540 | wgpu::TextureUsages::COPY_DST
541 | wgpu::TextureUsages::RENDER_ATTACHMENT,
542 view_formats: &[],
543 });
544 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
545 label: Some("Video Plane Sampler"),
546 address_mode_u: wgpu::AddressMode::ClampToEdge,
547 address_mode_v: wgpu::AddressMode::ClampToEdge,
548 address_mode_w: wgpu::AddressMode::ClampToEdge,
549 mag_filter: wgpu::FilterMode::Linear,
550 min_filter: wgpu::FilterMode::Linear,
551 mipmap_filter: wgpu::FilterMode::Nearest,
552 ..Default::default()
553 });
554 let output_sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
555 label: Some("Video Output Sampler"),
556 address_mode_u: wgpu::AddressMode::ClampToEdge,
557 address_mode_v: wgpu::AddressMode::ClampToEdge,
558 address_mode_w: wgpu::AddressMode::ClampToEdge,
559 mag_filter: wgpu::FilterMode::Linear,
560 min_filter: wgpu::FilterMode::Nearest,
561 mipmap_filter: wgpu::FilterMode::Nearest,
562 ..Default::default()
563 });
564 let conversion = YuvConversion::new(YuvColorInfo {
565 matrix: YuvMatrix::Bt709,
566 range: YuvRange::Limited,
567 });
568 let conversion_buffer = self
569 .device
570 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
571 label: Some("Video YUV Conversion Uniform"),
572 contents: bytemuck::bytes_of(&conversion),
573 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
574 });
575 let luma_view = luma.create_view(&wgpu::TextureViewDescriptor::default());
576 let chroma_view = chroma.create_view(&wgpu::TextureViewDescriptor::default());
577 let output_view = output.create_view(&wgpu::TextureViewDescriptor::default());
578 let conversion_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
579 label: Some("Video NV12 Conversion Bind Group"),
580 layout: &self.nv12_bind_group_layout,
581 entries: &[
582 wgpu::BindGroupEntry {
583 binding: 0,
584 resource: wgpu::BindingResource::TextureView(&luma_view),
585 },
586 wgpu::BindGroupEntry {
587 binding: 1,
588 resource: wgpu::BindingResource::TextureView(&chroma_view),
589 },
590 wgpu::BindGroupEntry {
591 binding: 2,
592 resource: wgpu::BindingResource::Sampler(&sampler),
593 },
594 wgpu::BindGroupEntry {
595 binding: 3,
596 resource: conversion_buffer.as_entire_binding(),
597 },
598 ],
599 });
600 let effects_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
601 label: Some("Video Effects Bind Group"),
602 layout: &self.bind_group_layout_tex,
603 entries: &[
604 wgpu::BindGroupEntry {
605 binding: 0,
606 resource: wgpu::BindingResource::TextureView(&output_view),
607 },
608 wgpu::BindGroupEntry {
609 binding: 1,
610 resource: wgpu::BindingResource::Sampler(&output_sampler),
611 },
612 ],
613 });
614
615 Ok(VideoTexture {
616 output,
617 output_view,
618 effects_bind_group,
619 luma,
620 chroma,
621 conversion_buffer,
622 conversion_bind_group,
623 width,
624 height,
625 })
626 }
627
628 pub fn validate_video_texture(&self, width: u32, height: u32) -> anyhow::Result<()> {
629 validate_texture_dimensions(width, height, self.device.limits().max_texture_dimension_2d)?;
630 validate_texture_memory(width, height, 6)
633 }
634
635 pub fn update_video_texture(
636 &self,
637 texture: &VideoTexture,
638 frame: &VideoFrameData,
639 ) -> anyhow::Result<()> {
640 match frame {
641 VideoFrameData::Rgba(rgba) => {
642 anyhow::ensure!(
643 rgba.len() == texture.width as usize * texture.height as usize * 4,
644 "invalid RGBA video frame size"
645 );
646 self.update_texture(&texture.output, rgba, texture.width, texture.height);
647 }
648 VideoFrameData::Nv12 {
649 y_plane,
650 uv_plane,
651 color,
652 } => {
653 let chroma_width = texture.width.div_ceil(2);
654 let chroma_height = texture.height.div_ceil(2);
655 anyhow::ensure!(
656 y_plane.len() == texture.width as usize * texture.height as usize,
657 "invalid NV12 luma plane size"
658 );
659 anyhow::ensure!(
660 uv_plane.len() == (chroma_width * chroma_height * 2) as usize,
661 "invalid NV12 chroma plane size"
662 );
663 self.queue.write_texture(
664 texture.luma.as_image_copy(),
665 y_plane,
666 wgpu::TexelCopyBufferLayout {
667 offset: 0,
668 bytes_per_row: Some(texture.width),
669 rows_per_image: Some(texture.height),
670 },
671 wgpu::Extent3d {
672 width: texture.width,
673 height: texture.height,
674 depth_or_array_layers: 1,
675 },
676 );
677 self.queue.write_texture(
678 texture.chroma.as_image_copy(),
679 uv_plane,
680 wgpu::TexelCopyBufferLayout {
681 offset: 0,
682 bytes_per_row: Some(chroma_width * 2),
683 rows_per_image: Some(chroma_height),
684 },
685 wgpu::Extent3d {
686 width: chroma_width,
687 height: chroma_height,
688 depth_or_array_layers: 1,
689 },
690 );
691 self.queue.write_buffer(
692 &texture.conversion_buffer,
693 0,
694 bytemuck::bytes_of(&YuvConversion::new(*color)),
695 );
696
697 let mut encoder =
698 self.device
699 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
700 label: Some("NV12 Conversion Encoder"),
701 });
702 {
703 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
704 label: Some("NV12 Conversion Pass"),
705 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
706 view: &texture.output_view,
707 resolve_target: None,
708 ops: wgpu::Operations {
709 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
710 store: wgpu::StoreOp::Store,
711 },
712 })],
713 depth_stencil_attachment: None,
714 occlusion_query_set: None,
715 timestamp_writes: None,
716 });
717 pass.set_pipeline(&self.nv12_pipeline);
718 pass.set_bind_group(0, &texture.conversion_bind_group, &[]);
719 pass.draw(0..3, 0..1);
720 }
721 self.queue.submit([encoder.finish()]);
722 }
723 }
724 Ok(())
725 }
726
727 pub fn load_texture(
728 &self,
729 image: &image::DynamicImage,
730 output_width: u32,
731 output_height: u32,
732 scaling_mode: u32,
733 ) -> anyhow::Result<(wgpu::Texture, wgpu::BindGroup, u32, u32)> {
734 let (width, height) = prepared_image_dimensions(
735 image.width(),
736 image.height(),
737 output_width,
738 output_height,
739 scaling_mode,
740 self.device.limits().max_texture_dimension_2d,
741 );
742 let rgba = if (width, height) == image.dimensions() {
743 image.to_rgba8()
744 } else {
745 image
746 .resize_exact(width, height, image::imageops::FilterType::Lanczos3)
747 .to_rgba8()
748 };
749 let (texture, bind_group) = self.create_texture(width, height)?;
750 self.update_texture(&texture, &rgba, width, height);
751 Ok((texture, bind_group, width, height))
752 }
753
754 pub fn update_uniforms(&self, buffer: &wgpu::Buffer, uniforms: Uniforms) {
755 self.queue
756 .write_buffer(buffer, 0, bytemuck::cast_slice(&[uniforms]));
757 }
758
759 pub fn render_frame(
760 &self,
761 request: FrameRequest,
762 per_output: &PerOutputUniforms,
763 ) -> anyhow::Result<FrameStatus> {
764 let FrameRequest {
765 surface,
766 format,
767 bg_bind,
768 new_bind,
769 effect,
770 width,
771 height,
772 img_width,
773 img_height,
774 old_img_width,
775 old_img_height,
776 scaling_mode,
777 } = request;
778 let mut uniforms = Uniforms::from_effect(effect);
779 uniforms.resolution = [width as f32, height as f32];
780 uniforms.image_resolution = [img_width as f32, img_height as f32];
781 uniforms.old_image_resolution = [old_img_width as f32, old_img_height as f32];
782 uniforms.scaling_mode = scaling_mode;
783 self.update_uniforms(&per_output.buffer, uniforms);
784
785 let pipeline = self.get_pipeline(format);
786 let output = match surface.get_current_texture() {
791 Ok(texture) => texture,
792 Err(wgpu::SurfaceError::Timeout) => return Ok(FrameStatus::TimedOut),
793 Err(wgpu::SurfaceError::Outdated) => return Ok(FrameStatus::Outdated),
794 Err(wgpu::SurfaceError::Lost) => return Ok(FrameStatus::Lost),
795 Err(err) => {
796 return Err(anyhow::anyhow!(
797 "failed to acquire swapchain texture: {err:?}"
798 ));
799 }
800 };
801 let view = output
802 .texture
803 .create_view(&wgpu::TextureViewDescriptor::default());
804
805 let mut encoder = self
806 .device
807 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
808 label: Some("Render Encoder"),
809 });
810
811 {
812 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
813 label: Some("Wallpaper Render Pass"),
814 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
815 view: &view,
816 resolve_target: None,
817 ops: wgpu::Operations {
818 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
819 store: wgpu::StoreOp::Store,
820 },
821 })],
822 depth_stencil_attachment: None,
823 occlusion_query_set: None,
824 timestamp_writes: None,
825 });
826
827 render_pass.set_pipeline(&pipeline);
828 render_pass.set_bind_group(0, bg_bind, &[]);
829 render_pass.set_bind_group(1, new_bind, &[]);
830 render_pass.set_bind_group(2, &per_output.bind_group, &[]);
831 render_pass.draw(0..3, 0..1);
833 }
834
835 self.queue.submit(std::iter::once(encoder.finish()));
836 output.present();
837
838 Ok(FrameStatus::Presented)
839 }
840}
841
842#[derive(Debug, Clone, Copy, PartialEq, Eq)]
844pub enum FrameStatus {
845 Presented,
846 TimedOut,
847 Outdated,
848 Lost,
849}
850
851fn validate_texture_dimensions(width: u32, height: u32, limit: u32) -> anyhow::Result<()> {
852 anyhow::ensure!(
853 width > 0 && height > 0,
854 "texture dimensions must be non-zero, got {width}x{height}"
855 );
856 anyhow::ensure!(
857 width <= limit && height <= limit,
858 "texture dimensions {width}x{height} exceed the GPU limit of {limit}"
859 );
860 Ok(())
861}
862
863fn validate_texture_memory(width: u32, height: u32, bytes_per_pixel: u64) -> anyhow::Result<()> {
864 validate_image_memory(
865 width,
866 height,
867 bytes_per_pixel,
868 MAX_TEXTURE_BYTES,
869 "texture allocation",
870 )
871}
872
873fn validate_image_memory(
874 width: u32,
875 height: u32,
876 bytes_per_pixel: u64,
877 limit: u64,
878 label: &str,
879) -> anyhow::Result<()> {
880 let bytes = u64::from(width)
881 .checked_mul(u64::from(height))
882 .and_then(|pixels| pixels.checked_mul(bytes_per_pixel))
883 .ok_or_else(|| anyhow::anyhow!("{label} size overflow for {width}x{height}"))?;
884 anyhow::ensure!(
885 bytes <= limit,
886 "{label} for {width}x{height} requires approximately {:.1} MiB, exceeding the {:.0} MiB safety limit",
887 bytes as f64 / (1024.0 * 1024.0),
888 limit as f64 / (1024.0 * 1024.0)
889 );
890 Ok(())
891}
892
893fn prepared_image_dimensions(
894 image_width: u32,
895 image_height: u32,
896 output_width: u32,
897 output_height: u32,
898 scaling_mode: u32,
899 texture_limit: u32,
900) -> (u32, u32) {
901 if image_width == 0 || image_height == 0 {
902 return (image_width, image_height);
903 }
904
905 let limit_scale = (texture_limit as f64 / image_width as f64)
906 .min(texture_limit as f64 / image_height as f64)
907 .min(1.0);
908 let output_scale = match scaling_mode {
909 0 if output_width > 0 && output_height > 0 => (output_width as f64 / image_width as f64)
912 .max(output_height as f64 / image_height as f64)
913 .min(1.0),
914 1 if output_width > 0 && output_height > 0 => (output_width as f64 / image_width as f64)
915 .min(output_height as f64 / image_height as f64)
916 .min(1.0),
917 2 if output_width > 0 && output_height > 0 => {
918 return (
919 image_width.min(output_width).min(texture_limit).max(1),
920 image_height.min(output_height).min(texture_limit).max(1),
921 );
922 }
923 _ => return (image_width, image_height),
926 };
927 let scale = limit_scale.min(output_scale);
928
929 (
930 ((image_width as f64 * scale).round() as u32).max(1),
931 ((image_height as f64 * scale).round() as u32).max(1),
932 )
933}
934
935pub struct FrameRequest<'a> {
937 pub surface: &'a wgpu::Surface<'a>,
938 pub format: wgpu::TextureFormat,
939 pub bg_bind: &'a wgpu::BindGroup,
941 pub new_bind: &'a wgpu::BindGroup,
943 pub effect: &'a crate::animation::EffectUniforms,
945 pub width: u32,
946 pub height: u32,
947 pub img_width: u32,
948 pub img_height: u32,
949 pub old_img_width: u32,
950 pub old_img_height: u32,
951 pub scaling_mode: u32,
953}
954
955#[cfg(test)]
956mod tests {
957 use super::*;
958
959 #[test]
960 fn selects_yuv_conversion_coefficients_and_range() {
961 let limited_709 = YuvConversion::new(YuvColorInfo {
962 matrix: YuvMatrix::Bt709,
963 range: YuvRange::Limited,
964 });
965 assert_eq!(limited_709.red, [1.0, 0.0, 1.5748, 0.0]);
966 assert_eq!(limited_709.green, [1.0, -0.187_324, -0.468_124, 0.0]);
967 assert_eq!(limited_709.range[0], 16.0 / 255.0);
968 assert_eq!(limited_709.range[1], 255.0 / 219.0);
969
970 let full_2020 = YuvConversion::new(YuvColorInfo {
971 matrix: YuvMatrix::Bt2020,
972 range: YuvRange::Full,
973 });
974 assert_eq!(full_2020.red, [1.0, 0.0, 1.4746, 0.0]);
975 assert_eq!(full_2020.blue, [1.0, 1.8814, 0.0, 0.0]);
976 assert_eq!(full_2020.range, [0.0, 1.0, 128.0 / 255.0, 1.0]);
977 }
978
979 #[test]
980 fn validates_texture_dimensions_before_wgpu() {
981 assert!(validate_texture_dimensions(8192, 8192, 8192).is_ok());
982 assert!(validate_texture_dimensions(0, 1080, 8192).is_err());
983 assert!(validate_texture_dimensions(8193, 1080, 8192).is_err());
984 assert!(validate_texture_dimensions(1920, 8193, 8192).is_err());
985 assert!(validate_texture_memory(7_680, 4_320, 6).is_ok());
986 assert!(validate_texture_memory(16_384, 16_384, 4).is_err());
987 assert!(Renderer::validate_static_decode(11_322, 6_192, 210_304_512).is_ok());
988 assert!(Renderer::validate_static_decode(32_768, 32_768, u64::MAX).is_err());
989 }
990
991 #[test]
992 fn fill_reduces_large_images_to_cover_the_output() {
993 assert_eq!(
994 prepared_image_dimensions(11_322, 6_192, 3_840, 2_160, 0, 32_768),
995 (3_950, 2_160)
996 );
997 }
998
999 #[test]
1000 fn fit_preserves_aspect_ratio_without_upscaling() {
1001 assert_eq!(
1002 prepared_image_dimensions(11_322, 6_192, 3_840, 2_160, 1, 32_768),
1003 (3_840, 2_100)
1004 );
1005 assert_eq!(
1006 prepared_image_dimensions(1_920, 1_080, 3_840, 2_160, 1, 32_768),
1007 (1_920, 1_080)
1008 );
1009 }
1010
1011 #[test]
1012 fn pixel_sensitive_modes_preserve_native_dimensions() {
1013 assert_eq!(
1014 prepared_image_dimensions(11_322, 6_192, 3_840, 2_160, 3, 8_192),
1015 (11_322, 6_192)
1016 );
1017 assert_eq!(
1018 prepared_image_dimensions(3_840, 2_160, 2_560, 1_440, 4, 8_192),
1019 (3_840, 2_160)
1020 );
1021 }
1022
1023 #[test]
1024 fn stretch_does_not_upscale_small_images() {
1025 assert_eq!(
1026 prepared_image_dimensions(1, 1, 3_840, 2_160, 2, 32_768),
1027 (1, 1)
1028 );
1029 assert_eq!(
1030 prepared_image_dimensions(5_000, 1_000, 3_840, 2_160, 2, 32_768),
1031 (3_840, 1_000)
1032 );
1033 }
1034}