Skip to main content

wallr_core/renderer/
mod.rs

1use image::GenericImageView;
2use wgpu::util::DeviceExt;
3
4use crate::video::{VideoFrameData, YuvColorInfo, YuvMatrix, YuvRange};
5
6pub struct Renderer {
7    pub instance: wgpu::Instance,
8    pub adapter: wgpu::Adapter,
9    pub device: wgpu::Device,
10    pub queue: wgpu::Queue,
11    pub bind_group_layout_tex: wgpu::BindGroupLayout,
12    pub bind_group_layout_uni: wgpu::BindGroupLayout,
13    pipeline_layout: wgpu::PipelineLayout,
14    shader: wgpu::ShaderModule,
15    /// Cached pipeline for a specific surface format. Created lazily.
16    pipeline: std::sync::Mutex<Option<(wgpu::TextureFormat, wgpu::RenderPipeline)>>,
17    nv12_bind_group_layout: wgpu::BindGroupLayout,
18    nv12_pipeline: wgpu::RenderPipeline,
19}
20
21pub struct VideoTexture {
22    output: wgpu::Texture,
23    output_view: wgpu::TextureView,
24    effects_bind_group: wgpu::BindGroup,
25    luma: wgpu::Texture,
26    chroma: wgpu::Texture,
27    conversion_buffer: wgpu::Buffer,
28    conversion_bind_group: wgpu::BindGroup,
29    width: u32,
30    height: u32,
31}
32
33impl VideoTexture {
34    pub fn texture(&self) -> &wgpu::Texture {
35        &self.output
36    }
37
38    pub fn bind_group(&self) -> &wgpu::BindGroup {
39        &self.effects_bind_group
40    }
41}
42
43#[repr(C)]
44#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
45struct YuvConversion {
46    range: [f32; 4],
47    red: [f32; 4],
48    green: [f32; 4],
49    blue: [f32; 4],
50}
51
52impl YuvConversion {
53    fn new(color: YuvColorInfo) -> Self {
54        let range = match color.range {
55            YuvRange::Limited => [16.0 / 255.0, 255.0 / 219.0, 128.0 / 255.0, 255.0 / 224.0],
56            YuvRange::Full => [0.0, 1.0, 128.0 / 255.0, 1.0],
57        };
58        let (red_cr, green_cb, green_cr, blue_cb) = match color.matrix {
59            YuvMatrix::Bt601 => (1.402, -0.344_136, -0.714_136, 1.772),
60            YuvMatrix::Bt709 => (1.5748, -0.187_324, -0.468_124, 1.8556),
61            YuvMatrix::Bt2020 => (1.4746, -0.164_553, -0.571_353, 1.8814),
62        };
63        Self {
64            range,
65            red: [1.0, 0.0, red_cr, 0.0],
66            green: [1.0, green_cb, green_cr, 0.0],
67            blue: [1.0, blue_cb, 0.0, 0.0],
68        }
69    }
70}
71
72/// Per-output uniform buffer and bind group. Each output gets its own so
73/// concurrent renders never race on shared GPU state.
74pub struct PerOutputUniforms {
75    pub buffer: wgpu::Buffer,
76    pub bind_group: wgpu::BindGroup,
77}
78
79#[repr(C)]
80#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
81pub struct Uniforms {
82    pub time: f32,
83    pub progress: f32,
84    pub effect_type: u32,
85    pub padding: u32,
86    pub resolution: [f32; 2],
87    pub image_resolution: [f32; 2],
88    pub old_image_resolution: [f32; 2],
89    pub param_a: f32,
90    pub param_b: f32,
91    pub param_c: f32,
92    pub param_d: f32,
93    pub origin: [f32; 2],
94    pub direction: [f32; 2],
95    pub easing: u32,
96    pub scaling_mode: u32,
97}
98
99impl Uniforms {
100    pub fn from_effect(effect: &crate::animation::EffectUniforms) -> Self {
101        Self {
102            time: effect.progress,
103            progress: effect.progress,
104            effect_type: effect.effect_type,
105            padding: 0,
106            resolution: [1920.0, 1080.0],
107            image_resolution: [1920.0, 1080.0],
108            old_image_resolution: [1920.0, 1080.0],
109            param_a: effect.param_a,
110            param_b: effect.param_b,
111            param_c: effect.param_c,
112            param_d: effect.param_d,
113            origin: effect.origin,
114            direction: effect.direction,
115            easing: effect.easing,
116            scaling_mode: 0,
117        }
118    }
119}
120
121impl Default for Uniforms {
122    fn default() -> Self {
123        Self {
124            time: 0.0,
125            progress: 0.0,
126            effect_type: 0,
127            padding: 0,
128            resolution: [1920.0, 1080.0],
129            image_resolution: [1920.0, 1080.0],
130            old_image_resolution: [1920.0, 1080.0],
131            param_a: 0.0,
132            param_b: 0.0,
133            param_c: 0.0,
134            param_d: 0.0,
135            origin: [0.5, 0.5],
136            direction: [0.0, 0.0],
137            easing: 3,
138            scaling_mode: 0,
139        }
140    }
141}
142
143impl Renderer {
144    pub async fn new() -> anyhow::Result<Self> {
145        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
146            backends: wgpu::Backends::all(),
147            ..Default::default()
148        });
149
150        let adapter = instance
151            .request_adapter(&wgpu::RequestAdapterOptions {
152                power_preference: wgpu::PowerPreference::HighPerformance,
153                compatible_surface: None,
154                force_fallback_adapter: false,
155            })
156            .await
157            .ok_or_else(|| anyhow::anyhow!("Failed to find suitable adapter"))?;
158
159        let (device, queue) = adapter
160            .request_device(
161                &wgpu::DeviceDescriptor {
162                    label: None,
163                    required_features: wgpu::Features::empty(),
164                    required_limits: wgpu::Limits::default(),
165                    memory_hints: wgpu::MemoryHints::default(),
166                },
167                None,
168            )
169            .await?;
170
171        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
172            label: Some("Effects Shader"),
173            source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
174                crate::shader::EFFECTS_SHADER,
175            )),
176        });
177
178        let bind_group_layout_tex =
179            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
180                entries: &[
181                    wgpu::BindGroupLayoutEntry {
182                        binding: 0,
183                        visibility: wgpu::ShaderStages::FRAGMENT,
184                        ty: wgpu::BindingType::Texture {
185                            multisampled: false,
186                            view_dimension: wgpu::TextureViewDimension::D2,
187                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
188                        },
189                        count: None,
190                    },
191                    wgpu::BindGroupLayoutEntry {
192                        binding: 1,
193                        visibility: wgpu::ShaderStages::FRAGMENT,
194                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
195                        count: None,
196                    },
197                ],
198                label: Some("texture_bind_group_layout"),
199            });
200
201        let bind_group_layout_uni =
202            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
203                entries: &[wgpu::BindGroupLayoutEntry {
204                    binding: 0,
205                    visibility: wgpu::ShaderStages::FRAGMENT,
206                    ty: wgpu::BindingType::Buffer {
207                        ty: wgpu::BufferBindingType::Uniform,
208                        has_dynamic_offset: false,
209                        min_binding_size: None,
210                    },
211                    count: None,
212                }],
213                label: Some("uniform_bind_group_layout"),
214            });
215
216        let nv12_bind_group_layout =
217            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
218                label: Some("NV12 Conversion Bind Group Layout"),
219                entries: &[
220                    wgpu::BindGroupLayoutEntry {
221                        binding: 0,
222                        visibility: wgpu::ShaderStages::FRAGMENT,
223                        ty: wgpu::BindingType::Texture {
224                            multisampled: false,
225                            view_dimension: wgpu::TextureViewDimension::D2,
226                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
227                        },
228                        count: None,
229                    },
230                    wgpu::BindGroupLayoutEntry {
231                        binding: 1,
232                        visibility: wgpu::ShaderStages::FRAGMENT,
233                        ty: wgpu::BindingType::Texture {
234                            multisampled: false,
235                            view_dimension: wgpu::TextureViewDimension::D2,
236                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
237                        },
238                        count: None,
239                    },
240                    wgpu::BindGroupLayoutEntry {
241                        binding: 2,
242                        visibility: wgpu::ShaderStages::FRAGMENT,
243                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
244                        count: None,
245                    },
246                    wgpu::BindGroupLayoutEntry {
247                        binding: 3,
248                        visibility: wgpu::ShaderStages::FRAGMENT,
249                        ty: wgpu::BindingType::Buffer {
250                            ty: wgpu::BufferBindingType::Uniform,
251                            has_dynamic_offset: false,
252                            min_binding_size: None,
253                        },
254                        count: None,
255                    },
256                ],
257            });
258
259        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
260            label: Some("Render Pipeline Layout"),
261            bind_group_layouts: &[
262                &bind_group_layout_tex,
263                &bind_group_layout_tex,
264                &bind_group_layout_uni,
265            ],
266            push_constant_ranges: &[],
267        });
268
269        let nv12_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
270            label: Some("NV12 to RGB Shader"),
271            source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
272                crate::shader::NV12_TO_RGB_SHADER,
273            )),
274        });
275        let nv12_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
276            label: Some("NV12 Conversion Pipeline Layout"),
277            bind_group_layouts: &[&nv12_bind_group_layout],
278            push_constant_ranges: &[],
279        });
280        let nv12_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
281            label: Some("NV12 Conversion Pipeline"),
282            layout: Some(&nv12_pipeline_layout),
283            vertex: wgpu::VertexState {
284                module: &nv12_shader,
285                entry_point: Some("vs_main"),
286                buffers: &[],
287                compilation_options: wgpu::PipelineCompilationOptions::default(),
288            },
289            fragment: Some(wgpu::FragmentState {
290                module: &nv12_shader,
291                entry_point: Some("fs_main"),
292                targets: &[Some(wgpu::ColorTargetState {
293                    format: wgpu::TextureFormat::Rgba8UnormSrgb,
294                    blend: Some(wgpu::BlendState::REPLACE),
295                    write_mask: wgpu::ColorWrites::ALL,
296                })],
297                compilation_options: wgpu::PipelineCompilationOptions::default(),
298            }),
299            primitive: wgpu::PrimitiveState::default(),
300            depth_stencil: None,
301            multisample: wgpu::MultisampleState::default(),
302            multiview: None,
303            cache: None,
304        });
305
306        Ok(Self {
307            instance,
308            adapter,
309            device,
310            queue,
311            pipeline_layout,
312            shader,
313            bind_group_layout_tex,
314            bind_group_layout_uni,
315            pipeline: std::sync::Mutex::new(None),
316            nv12_bind_group_layout,
317            nv12_pipeline,
318        })
319    }
320
321    /// Create a per-output uniform buffer and bind group so each output
322    /// renders with its own GPU state, eliminating cross-output races.
323    pub fn create_per_output_uniforms(&self) -> PerOutputUniforms {
324        let uniforms = Uniforms::default();
325        let buffer = self
326            .device
327            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
328                label: Some("Per-Output Uniform Buffer"),
329                contents: bytemuck::cast_slice(&[uniforms]),
330                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
331            });
332        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
333            layout: &self.bind_group_layout_uni,
334            entries: &[wgpu::BindGroupEntry {
335                binding: 0,
336                resource: buffer.as_entire_binding(),
337            }],
338            label: Some("per_output_uniform_bind_group"),
339        });
340        PerOutputUniforms { buffer, bind_group }
341    }
342
343    fn get_pipeline(&self, format: wgpu::TextureFormat) -> wgpu::RenderPipeline {
344        let mut cache = self.pipeline.lock().unwrap();
345        if let Some((cached_fmt, ref pipeline)) = *cache
346            && cached_fmt == format
347        {
348            return pipeline.clone();
349        }
350
351        let pipeline = self
352            .device
353            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
354                label: Some("Render Pipeline"),
355                layout: Some(&self.pipeline_layout),
356                vertex: wgpu::VertexState {
357                    module: &self.shader,
358                    entry_point: Some("vs_main"),
359                    buffers: &[],
360                    compilation_options: wgpu::PipelineCompilationOptions::default(),
361                },
362                fragment: Some(wgpu::FragmentState {
363                    module: &self.shader,
364                    entry_point: Some("fs_main"),
365                    targets: &[Some(wgpu::ColorTargetState {
366                        format,
367                        blend: Some(wgpu::BlendState::REPLACE),
368                        write_mask: wgpu::ColorWrites::ALL,
369                    })],
370                    compilation_options: wgpu::PipelineCompilationOptions::default(),
371                }),
372                primitive: wgpu::PrimitiveState {
373                    topology: wgpu::PrimitiveTopology::TriangleList,
374                    strip_index_format: None,
375                    front_face: wgpu::FrontFace::Ccw,
376                    cull_mode: None, // Don't cull — fullscreen quad
377                    polygon_mode: wgpu::PolygonMode::Fill,
378                    unclipped_depth: false,
379                    conservative: false,
380                },
381                depth_stencil: None,
382                multisample: wgpu::MultisampleState {
383                    count: 1,
384                    mask: !0,
385                    alpha_to_coverage_enabled: false,
386                },
387                multiview: None,
388                cache: None,
389            });
390
391        *cache = Some((format, pipeline.clone()));
392        pipeline
393    }
394
395    pub fn create_texture(&self, width: u32, height: u32) -> (wgpu::Texture, wgpu::BindGroup) {
396        let size = wgpu::Extent3d {
397            width,
398            height,
399            depth_or_array_layers: 1,
400        };
401
402        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
403            label: None,
404            size,
405            mip_level_count: 1,
406            sample_count: 1,
407            dimension: wgpu::TextureDimension::D2,
408            format: wgpu::TextureFormat::Rgba8UnormSrgb,
409            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
410            view_formats: &[],
411        });
412
413        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
414        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
415            address_mode_u: wgpu::AddressMode::ClampToEdge,
416            address_mode_v: wgpu::AddressMode::ClampToEdge,
417            address_mode_w: wgpu::AddressMode::ClampToEdge,
418            mag_filter: wgpu::FilterMode::Linear,
419            min_filter: wgpu::FilterMode::Nearest,
420            mipmap_filter: wgpu::FilterMode::Nearest,
421            ..Default::default()
422        });
423        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
424            layout: &self.bind_group_layout_tex,
425            entries: &[
426                wgpu::BindGroupEntry {
427                    binding: 0,
428                    resource: wgpu::BindingResource::TextureView(&view),
429                },
430                wgpu::BindGroupEntry {
431                    binding: 1,
432                    resource: wgpu::BindingResource::Sampler(&sampler),
433                },
434            ],
435            label: None,
436        });
437
438        (texture, bind_group)
439    }
440
441    pub fn update_texture(&self, texture: &wgpu::Texture, rgba: &[u8], width: u32, height: u32) {
442        self.queue.write_texture(
443            wgpu::TexelCopyTextureInfo {
444                texture,
445                mip_level: 0,
446                origin: wgpu::Origin3d::ZERO,
447                aspect: wgpu::TextureAspect::All,
448            },
449            rgba,
450            wgpu::TexelCopyBufferLayout {
451                offset: 0,
452                bytes_per_row: Some(4 * width),
453                rows_per_image: Some(height),
454            },
455            wgpu::Extent3d {
456                width,
457                height,
458                depth_or_array_layers: 1,
459            },
460        );
461    }
462
463    pub fn create_video_texture(&self, width: u32, height: u32) -> VideoTexture {
464        let plane_texture = |label, size, format| {
465            self.device.create_texture(&wgpu::TextureDescriptor {
466                label: Some(label),
467                size,
468                mip_level_count: 1,
469                sample_count: 1,
470                dimension: wgpu::TextureDimension::D2,
471                format,
472                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
473                view_formats: &[],
474            })
475        };
476        let luma = plane_texture(
477            "Video NV12 Luma",
478            wgpu::Extent3d {
479                width,
480                height,
481                depth_or_array_layers: 1,
482            },
483            wgpu::TextureFormat::R8Unorm,
484        );
485        let chroma = plane_texture(
486            "Video NV12 Chroma",
487            wgpu::Extent3d {
488                width: width.div_ceil(2),
489                height: height.div_ceil(2),
490                depth_or_array_layers: 1,
491            },
492            wgpu::TextureFormat::Rg8Unorm,
493        );
494        let output = self.device.create_texture(&wgpu::TextureDescriptor {
495            label: Some("Video RGB Output"),
496            size: wgpu::Extent3d {
497                width,
498                height,
499                depth_or_array_layers: 1,
500            },
501            mip_level_count: 1,
502            sample_count: 1,
503            dimension: wgpu::TextureDimension::D2,
504            format: wgpu::TextureFormat::Rgba8UnormSrgb,
505            usage: wgpu::TextureUsages::TEXTURE_BINDING
506                | wgpu::TextureUsages::COPY_DST
507                | wgpu::TextureUsages::RENDER_ATTACHMENT,
508            view_formats: &[],
509        });
510        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
511            label: Some("Video Plane Sampler"),
512            address_mode_u: wgpu::AddressMode::ClampToEdge,
513            address_mode_v: wgpu::AddressMode::ClampToEdge,
514            address_mode_w: wgpu::AddressMode::ClampToEdge,
515            mag_filter: wgpu::FilterMode::Linear,
516            min_filter: wgpu::FilterMode::Linear,
517            mipmap_filter: wgpu::FilterMode::Nearest,
518            ..Default::default()
519        });
520        let output_sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
521            label: Some("Video Output Sampler"),
522            address_mode_u: wgpu::AddressMode::ClampToEdge,
523            address_mode_v: wgpu::AddressMode::ClampToEdge,
524            address_mode_w: wgpu::AddressMode::ClampToEdge,
525            mag_filter: wgpu::FilterMode::Linear,
526            min_filter: wgpu::FilterMode::Nearest,
527            mipmap_filter: wgpu::FilterMode::Nearest,
528            ..Default::default()
529        });
530        let conversion = YuvConversion::new(YuvColorInfo {
531            matrix: YuvMatrix::Bt709,
532            range: YuvRange::Limited,
533        });
534        let conversion_buffer = self
535            .device
536            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
537                label: Some("Video YUV Conversion Uniform"),
538                contents: bytemuck::bytes_of(&conversion),
539                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
540            });
541        let luma_view = luma.create_view(&wgpu::TextureViewDescriptor::default());
542        let chroma_view = chroma.create_view(&wgpu::TextureViewDescriptor::default());
543        let output_view = output.create_view(&wgpu::TextureViewDescriptor::default());
544        let conversion_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
545            label: Some("Video NV12 Conversion Bind Group"),
546            layout: &self.nv12_bind_group_layout,
547            entries: &[
548                wgpu::BindGroupEntry {
549                    binding: 0,
550                    resource: wgpu::BindingResource::TextureView(&luma_view),
551                },
552                wgpu::BindGroupEntry {
553                    binding: 1,
554                    resource: wgpu::BindingResource::TextureView(&chroma_view),
555                },
556                wgpu::BindGroupEntry {
557                    binding: 2,
558                    resource: wgpu::BindingResource::Sampler(&sampler),
559                },
560                wgpu::BindGroupEntry {
561                    binding: 3,
562                    resource: conversion_buffer.as_entire_binding(),
563                },
564            ],
565        });
566        let effects_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
567            label: Some("Video Effects Bind Group"),
568            layout: &self.bind_group_layout_tex,
569            entries: &[
570                wgpu::BindGroupEntry {
571                    binding: 0,
572                    resource: wgpu::BindingResource::TextureView(&output_view),
573                },
574                wgpu::BindGroupEntry {
575                    binding: 1,
576                    resource: wgpu::BindingResource::Sampler(&output_sampler),
577                },
578            ],
579        });
580
581        VideoTexture {
582            output,
583            output_view,
584            effects_bind_group,
585            luma,
586            chroma,
587            conversion_buffer,
588            conversion_bind_group,
589            width,
590            height,
591        }
592    }
593
594    pub fn update_video_texture(
595        &self,
596        texture: &VideoTexture,
597        frame: &VideoFrameData,
598    ) -> anyhow::Result<()> {
599        match frame {
600            VideoFrameData::Rgba(rgba) => {
601                anyhow::ensure!(
602                    rgba.len() == texture.width as usize * texture.height as usize * 4,
603                    "invalid RGBA video frame size"
604                );
605                self.update_texture(&texture.output, rgba, texture.width, texture.height);
606            }
607            VideoFrameData::Nv12 {
608                y_plane,
609                uv_plane,
610                color,
611            } => {
612                let chroma_width = texture.width.div_ceil(2);
613                let chroma_height = texture.height.div_ceil(2);
614                anyhow::ensure!(
615                    y_plane.len() == texture.width as usize * texture.height as usize,
616                    "invalid NV12 luma plane size"
617                );
618                anyhow::ensure!(
619                    uv_plane.len() == (chroma_width * chroma_height * 2) as usize,
620                    "invalid NV12 chroma plane size"
621                );
622                self.queue.write_texture(
623                    texture.luma.as_image_copy(),
624                    y_plane,
625                    wgpu::TexelCopyBufferLayout {
626                        offset: 0,
627                        bytes_per_row: Some(texture.width),
628                        rows_per_image: Some(texture.height),
629                    },
630                    wgpu::Extent3d {
631                        width: texture.width,
632                        height: texture.height,
633                        depth_or_array_layers: 1,
634                    },
635                );
636                self.queue.write_texture(
637                    texture.chroma.as_image_copy(),
638                    uv_plane,
639                    wgpu::TexelCopyBufferLayout {
640                        offset: 0,
641                        bytes_per_row: Some(chroma_width * 2),
642                        rows_per_image: Some(chroma_height),
643                    },
644                    wgpu::Extent3d {
645                        width: chroma_width,
646                        height: chroma_height,
647                        depth_or_array_layers: 1,
648                    },
649                );
650                self.queue.write_buffer(
651                    &texture.conversion_buffer,
652                    0,
653                    bytemuck::bytes_of(&YuvConversion::new(*color)),
654                );
655
656                let mut encoder =
657                    self.device
658                        .create_command_encoder(&wgpu::CommandEncoderDescriptor {
659                            label: Some("NV12 Conversion Encoder"),
660                        });
661                {
662                    let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
663                        label: Some("NV12 Conversion Pass"),
664                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
665                            view: &texture.output_view,
666                            resolve_target: None,
667                            ops: wgpu::Operations {
668                                load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
669                                store: wgpu::StoreOp::Store,
670                            },
671                        })],
672                        depth_stencil_attachment: None,
673                        occlusion_query_set: None,
674                        timestamp_writes: None,
675                    });
676                    pass.set_pipeline(&self.nv12_pipeline);
677                    pass.set_bind_group(0, &texture.conversion_bind_group, &[]);
678                    pass.draw(0..3, 0..1);
679                }
680                self.queue.submit([encoder.finish()]);
681            }
682        }
683        Ok(())
684    }
685
686    pub fn load_texture(
687        &self,
688        image: &image::DynamicImage,
689    ) -> anyhow::Result<(wgpu::Texture, wgpu::BindGroup)> {
690        let rgba = image.to_rgba8();
691        let (width, height) = image.dimensions();
692        let (texture, bind_group) = self.create_texture(width, height);
693        self.update_texture(&texture, &rgba, width, height);
694        Ok((texture, bind_group))
695    }
696
697    pub fn update_uniforms(&self, buffer: &wgpu::Buffer, uniforms: Uniforms) {
698        self.queue
699            .write_buffer(buffer, 0, bytemuck::cast_slice(&[uniforms]));
700    }
701
702    pub fn render_frame(
703        &self,
704        request: FrameRequest,
705        per_output: &PerOutputUniforms,
706    ) -> anyhow::Result<FrameStatus> {
707        let FrameRequest {
708            surface,
709            format,
710            bg_bind,
711            new_bind,
712            effect,
713            width,
714            height,
715            img_width,
716            img_height,
717            old_img_width,
718            old_img_height,
719            scaling_mode,
720        } = request;
721        let mut uniforms = Uniforms::from_effect(effect);
722        uniforms.resolution = [width as f32, height as f32];
723        uniforms.image_resolution = [img_width as f32, img_height as f32];
724        uniforms.old_image_resolution = [old_img_width as f32, old_img_height as f32];
725        uniforms.scaling_mode = scaling_mode;
726        self.update_uniforms(&per_output.buffer, uniforms);
727
728        let pipeline = self.get_pipeline(format);
729        // `get_current_texture` blocks until the compositor presents (Fifo),
730        // which paces rendering to the refresh rate. A stalled compositor
731        // parks this call, which is safe because transition loops always run
732        // on detached tasks that never block the daemon's IPC loop.
733        let output = match surface.get_current_texture() {
734            Ok(texture) => texture,
735            Err(wgpu::SurfaceError::Timeout) => return Ok(FrameStatus::TimedOut),
736            Err(wgpu::SurfaceError::Outdated) => return Ok(FrameStatus::Outdated),
737            Err(wgpu::SurfaceError::Lost) => return Ok(FrameStatus::Lost),
738            Err(err) => {
739                return Err(anyhow::anyhow!(
740                    "failed to acquire swapchain texture: {err:?}"
741                ));
742            }
743        };
744        let view = output
745            .texture
746            .create_view(&wgpu::TextureViewDescriptor::default());
747
748        let mut encoder = self
749            .device
750            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
751                label: Some("Render Encoder"),
752            });
753
754        {
755            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
756                label: Some("Wallpaper Render Pass"),
757                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
758                    view: &view,
759                    resolve_target: None,
760                    ops: wgpu::Operations {
761                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
762                        store: wgpu::StoreOp::Store,
763                    },
764                })],
765                depth_stencil_attachment: None,
766                occlusion_query_set: None,
767                timestamp_writes: None,
768            });
769
770            render_pass.set_pipeline(&pipeline);
771            render_pass.set_bind_group(0, bg_bind, &[]);
772            render_pass.set_bind_group(1, new_bind, &[]);
773            render_pass.set_bind_group(2, &per_output.bind_group, &[]);
774            // The vertex shader generates a fullscreen triangle from vertex_index 0..3
775            render_pass.draw(0..3, 0..1);
776        }
777
778        self.queue.submit(std::iter::once(encoder.finish()));
779        output.present();
780
781        Ok(FrameStatus::Presented)
782    }
783}
784
785/// Whether a frame was presented or the surface needs recovery.
786#[derive(Debug, Clone, Copy, PartialEq, Eq)]
787pub enum FrameStatus {
788    Presented,
789    TimedOut,
790    Outdated,
791    Lost,
792}
793
794/// Everything needed to present one transition frame to a surface.
795pub struct FrameRequest<'a> {
796    pub surface: &'a wgpu::Surface<'a>,
797    pub format: wgpu::TextureFormat,
798    /// Outgoing wallpaper frame, normally the daemon's previous wallpaper.
799    pub bg_bind: &'a wgpu::BindGroup,
800    /// New wallpaper.
801    pub new_bind: &'a wgpu::BindGroup,
802    /// Effect uniforms for this frame (progress, params, origin, easing...).
803    pub effect: &'a crate::animation::EffectUniforms,
804    pub width: u32,
805    pub height: u32,
806    pub img_width: u32,
807    pub img_height: u32,
808    pub old_img_width: u32,
809    pub old_img_height: u32,
810    /// Scaling mode: 0=Fill, 1=Fit, 2=Stretch, 3=Center, 4=Tile.
811    pub scaling_mode: u32,
812}
813
814#[cfg(test)]
815mod tests {
816    use super::*;
817
818    #[test]
819    fn selects_yuv_conversion_coefficients_and_range() {
820        let limited_709 = YuvConversion::new(YuvColorInfo {
821            matrix: YuvMatrix::Bt709,
822            range: YuvRange::Limited,
823        });
824        assert_eq!(limited_709.red, [1.0, 0.0, 1.5748, 0.0]);
825        assert_eq!(limited_709.green, [1.0, -0.187_324, -0.468_124, 0.0]);
826        assert_eq!(limited_709.range[0], 16.0 / 255.0);
827        assert_eq!(limited_709.range[1], 255.0 / 219.0);
828
829        let full_2020 = YuvConversion::new(YuvColorInfo {
830            matrix: YuvMatrix::Bt2020,
831            range: YuvRange::Full,
832        });
833        assert_eq!(full_2020.red, [1.0, 0.0, 1.4746, 0.0]);
834        assert_eq!(full_2020.blue, [1.0, 1.8814, 0.0, 0.0]);
835        assert_eq!(full_2020.range, [0.0, 1.0, 128.0 / 255.0, 1.0]);
836    }
837}