Skip to main content

wallr_core/renderer/
mod.rs

1use image::GenericImageView;
2use wgpu::util::DeviceExt;
3
4pub struct Renderer {
5    pub instance: wgpu::Instance,
6    pub adapter: wgpu::Adapter,
7    pub device: wgpu::Device,
8    pub queue: wgpu::Queue,
9    pub bind_group_layout_tex: wgpu::BindGroupLayout,
10    pub bind_group_layout_uni: wgpu::BindGroupLayout,
11    pub uniform_buffer: wgpu::Buffer,
12    pub uniform_bind_group: wgpu::BindGroup,
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}
18
19#[repr(C)]
20#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
21pub struct Uniforms {
22    pub time: f32,
23    pub progress: f32,
24    pub effect_type: u32,
25    pub padding: u32,
26    pub resolution: [f32; 2],
27    pub image_resolution: [f32; 2],
28    pub old_image_resolution: [f32; 2],
29    pub param_a: f32,
30    pub param_b: f32,
31    pub param_c: f32,
32    pub param_d: f32,
33    pub origin: [f32; 2],
34    pub direction: [f32; 2],
35    pub easing: u32,
36    pub scaling_mode: u32,
37}
38
39impl Uniforms {
40    pub fn from_effect(effect: &crate::animation::EffectUniforms) -> Self {
41        Self {
42            time: effect.progress,
43            progress: effect.progress,
44            effect_type: effect.effect_type,
45            padding: 0,
46            resolution: [1920.0, 1080.0],
47            image_resolution: [1920.0, 1080.0],
48            old_image_resolution: [1920.0, 1080.0],
49            param_a: effect.param_a,
50            param_b: effect.param_b,
51            param_c: effect.param_c,
52            param_d: effect.param_d,
53            origin: effect.origin,
54            direction: effect.direction,
55            easing: effect.easing,
56            scaling_mode: 0,
57        }
58    }
59}
60
61impl Renderer {
62    pub async fn new() -> anyhow::Result<Self> {
63        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
64            backends: wgpu::Backends::all(),
65            ..Default::default()
66        });
67
68        let adapter = instance
69            .request_adapter(&wgpu::RequestAdapterOptions {
70                power_preference: wgpu::PowerPreference::HighPerformance,
71                compatible_surface: None,
72                force_fallback_adapter: false,
73            })
74            .await
75            .ok_or_else(|| anyhow::anyhow!("Failed to find suitable adapter"))?;
76
77        let (device, queue) = adapter
78            .request_device(
79                &wgpu::DeviceDescriptor {
80                    label: None,
81                    required_features: wgpu::Features::empty(),
82                    required_limits: wgpu::Limits::default(),
83                    memory_hints: wgpu::MemoryHints::default(),
84                },
85                None,
86            )
87            .await?;
88
89        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
90            label: Some("Effects Shader"),
91            source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
92                crate::shader::EFFECTS_SHADER,
93            )),
94        });
95
96        let bind_group_layout_tex =
97            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
98                entries: &[
99                    wgpu::BindGroupLayoutEntry {
100                        binding: 0,
101                        visibility: wgpu::ShaderStages::FRAGMENT,
102                        ty: wgpu::BindingType::Texture {
103                            multisampled: false,
104                            view_dimension: wgpu::TextureViewDimension::D2,
105                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
106                        },
107                        count: None,
108                    },
109                    wgpu::BindGroupLayoutEntry {
110                        binding: 1,
111                        visibility: wgpu::ShaderStages::FRAGMENT,
112                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
113                        count: None,
114                    },
115                ],
116                label: Some("texture_bind_group_layout"),
117            });
118
119        let bind_group_layout_uni =
120            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
121                entries: &[wgpu::BindGroupLayoutEntry {
122                    binding: 0,
123                    visibility: wgpu::ShaderStages::FRAGMENT,
124                    ty: wgpu::BindingType::Buffer {
125                        ty: wgpu::BufferBindingType::Uniform,
126                        has_dynamic_offset: false,
127                        min_binding_size: None,
128                    },
129                    count: None,
130                }],
131                label: Some("uniform_bind_group_layout"),
132            });
133
134        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
135            label: Some("Render Pipeline Layout"),
136            bind_group_layouts: &[
137                &bind_group_layout_tex,
138                &bind_group_layout_tex,
139                &bind_group_layout_uni,
140            ],
141            push_constant_ranges: &[],
142        });
143
144        let uniforms = Uniforms {
145            time: 0.0,
146            progress: 0.0,
147            effect_type: 0,
148            padding: 0,
149            resolution: [1920.0, 1080.0],
150            image_resolution: [1920.0, 1080.0],
151            old_image_resolution: [1920.0, 1080.0],
152            param_a: 0.0,
153            param_b: 0.0,
154            param_c: 0.0,
155            param_d: 0.0,
156            origin: [0.5, 0.5],
157            direction: [0.0, 0.0],
158            easing: 3,
159            scaling_mode: 0,
160        };
161
162        let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
163            label: Some("Uniform Buffer"),
164            contents: bytemuck::cast_slice(&[uniforms]),
165            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
166        });
167
168        let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
169            layout: &bind_group_layout_uni,
170            entries: &[wgpu::BindGroupEntry {
171                binding: 0,
172                resource: uniform_buffer.as_entire_binding(),
173            }],
174            label: Some("uniform_bind_group"),
175        });
176
177        Ok(Self {
178            instance,
179            adapter,
180            device,
181            queue,
182            pipeline_layout,
183            shader,
184            bind_group_layout_tex,
185            bind_group_layout_uni,
186            uniform_buffer,
187            uniform_bind_group,
188            pipeline: std::sync::Mutex::new(None),
189        })
190    }
191
192    fn get_pipeline(&self, format: wgpu::TextureFormat) -> wgpu::RenderPipeline {
193        let mut cache = self.pipeline.lock().unwrap();
194        if let Some((cached_fmt, ref pipeline)) = *cache
195            && cached_fmt == format
196        {
197            return pipeline.clone();
198        }
199
200        let pipeline = self
201            .device
202            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
203                label: Some("Render Pipeline"),
204                layout: Some(&self.pipeline_layout),
205                vertex: wgpu::VertexState {
206                    module: &self.shader,
207                    entry_point: Some("vs_main"),
208                    buffers: &[],
209                    compilation_options: wgpu::PipelineCompilationOptions::default(),
210                },
211                fragment: Some(wgpu::FragmentState {
212                    module: &self.shader,
213                    entry_point: Some("fs_main"),
214                    targets: &[Some(wgpu::ColorTargetState {
215                        format,
216                        blend: Some(wgpu::BlendState::REPLACE),
217                        write_mask: wgpu::ColorWrites::ALL,
218                    })],
219                    compilation_options: wgpu::PipelineCompilationOptions::default(),
220                }),
221                primitive: wgpu::PrimitiveState {
222                    topology: wgpu::PrimitiveTopology::TriangleList,
223                    strip_index_format: None,
224                    front_face: wgpu::FrontFace::Ccw,
225                    cull_mode: None, // Don't cull — fullscreen quad
226                    polygon_mode: wgpu::PolygonMode::Fill,
227                    unclipped_depth: false,
228                    conservative: false,
229                },
230                depth_stencil: None,
231                multisample: wgpu::MultisampleState {
232                    count: 1,
233                    mask: !0,
234                    alpha_to_coverage_enabled: false,
235                },
236                multiview: None,
237                cache: None,
238            });
239
240        *cache = Some((format, pipeline.clone()));
241        pipeline
242    }
243
244    pub fn create_texture(&self, width: u32, height: u32) -> (wgpu::Texture, wgpu::BindGroup) {
245        let size = wgpu::Extent3d {
246            width,
247            height,
248            depth_or_array_layers: 1,
249        };
250
251        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
252            label: None,
253            size,
254            mip_level_count: 1,
255            sample_count: 1,
256            dimension: wgpu::TextureDimension::D2,
257            format: wgpu::TextureFormat::Rgba8UnormSrgb,
258            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
259            view_formats: &[],
260        });
261
262        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
263        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
264            address_mode_u: wgpu::AddressMode::ClampToEdge,
265            address_mode_v: wgpu::AddressMode::ClampToEdge,
266            address_mode_w: wgpu::AddressMode::ClampToEdge,
267            mag_filter: wgpu::FilterMode::Linear,
268            min_filter: wgpu::FilterMode::Nearest,
269            mipmap_filter: wgpu::FilterMode::Nearest,
270            ..Default::default()
271        });
272
273        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
274            layout: &self.bind_group_layout_tex,
275            entries: &[
276                wgpu::BindGroupEntry {
277                    binding: 0,
278                    resource: wgpu::BindingResource::TextureView(&view),
279                },
280                wgpu::BindGroupEntry {
281                    binding: 1,
282                    resource: wgpu::BindingResource::Sampler(&sampler),
283                },
284            ],
285            label: None,
286        });
287
288        (texture, bind_group)
289    }
290
291    pub fn update_texture(&self, texture: &wgpu::Texture, rgba: &[u8], width: u32, height: u32) {
292        self.queue.write_texture(
293            wgpu::TexelCopyTextureInfo {
294                texture,
295                mip_level: 0,
296                origin: wgpu::Origin3d::ZERO,
297                aspect: wgpu::TextureAspect::All,
298            },
299            rgba,
300            wgpu::TexelCopyBufferLayout {
301                offset: 0,
302                bytes_per_row: Some(4 * width),
303                rows_per_image: Some(height),
304            },
305            wgpu::Extent3d {
306                width,
307                height,
308                depth_or_array_layers: 1,
309            },
310        );
311    }
312
313    pub fn load_texture(
314        &self,
315        image: &image::DynamicImage,
316    ) -> anyhow::Result<(wgpu::Texture, wgpu::BindGroup)> {
317        let rgba = image.to_rgba8();
318        let (width, height) = image.dimensions();
319        let (texture, bind_group) = self.create_texture(width, height);
320        self.update_texture(&texture, &rgba, width, height);
321        Ok((texture, bind_group))
322    }
323
324    pub fn update_uniforms(&self, uniforms: Uniforms) {
325        self.queue
326            .write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
327    }
328
329    pub fn render_frame(&self, request: FrameRequest) -> anyhow::Result<FrameStatus> {
330        let FrameRequest {
331            surface,
332            format,
333            bg_bind,
334            new_bind,
335            effect,
336            width,
337            height,
338            img_width,
339            img_height,
340            old_img_width,
341            old_img_height,
342            scaling_mode,
343        } = request;
344        let mut uniforms = Uniforms::from_effect(effect);
345        uniforms.resolution = [width as f32, height as f32];
346        uniforms.image_resolution = [img_width as f32, img_height as f32];
347        uniforms.old_image_resolution = [old_img_width as f32, old_img_height as f32];
348        uniforms.scaling_mode = scaling_mode;
349        self.update_uniforms(uniforms);
350
351        let pipeline = self.get_pipeline(format);
352        // `get_current_texture` blocks until the compositor presents (Fifo),
353        // which paces rendering to the refresh rate. A stalled compositor
354        // parks this call, which is safe because transition loops always run
355        // on detached tasks that never block the daemon's IPC loop.
356        let output = match surface.get_current_texture() {
357            Ok(texture) => texture,
358            Err(wgpu::SurfaceError::Timeout) => return Ok(FrameStatus::TimedOut),
359            Err(err) => {
360                return Err(anyhow::anyhow!(
361                    "failed to acquire swapchain texture: {err:?}"
362                ));
363            }
364        };
365        let view = output
366            .texture
367            .create_view(&wgpu::TextureViewDescriptor::default());
368
369        let mut encoder = self
370            .device
371            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
372                label: Some("Render Encoder"),
373            });
374
375        {
376            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
377                label: Some("Wallpaper Render Pass"),
378                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
379                    view: &view,
380                    resolve_target: None,
381                    ops: wgpu::Operations {
382                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
383                        store: wgpu::StoreOp::Store,
384                    },
385                })],
386                depth_stencil_attachment: None,
387                occlusion_query_set: None,
388                timestamp_writes: None,
389            });
390
391            render_pass.set_pipeline(&pipeline);
392            render_pass.set_bind_group(0, bg_bind, &[]);
393            render_pass.set_bind_group(1, new_bind, &[]);
394            render_pass.set_bind_group(2, &self.uniform_bind_group, &[]);
395            // The vertex shader generates a fullscreen triangle from vertex_index 0..3
396            render_pass.draw(0..3, 0..1);
397        }
398
399        self.queue.submit(std::iter::once(encoder.finish()));
400        output.present();
401
402        Ok(FrameStatus::Presented)
403    }
404}
405
406/// Whether a frame was actually presented to the surface. A `TimedOut` frame
407/// means the compositor is not requesting frames right now (e.g. the monitor
408/// is off), and callers should stop rendering instead of spinning.
409#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410pub enum FrameStatus {
411    Presented,
412    TimedOut,
413}
414
415/// Everything needed to present one transition frame to a surface.
416pub struct FrameRequest<'a> {
417    pub surface: &'a wgpu::Surface<'a>,
418    pub format: wgpu::TextureFormat,
419    /// Outgoing wallpaper frame, normally the daemon's previous wallpaper.
420    pub bg_bind: &'a wgpu::BindGroup,
421    /// New wallpaper.
422    pub new_bind: &'a wgpu::BindGroup,
423    /// Effect uniforms for this frame (progress, params, origin, easing...).
424    pub effect: &'a crate::animation::EffectUniforms,
425    pub width: u32,
426    pub height: u32,
427    pub img_width: u32,
428    pub img_height: u32,
429    pub old_img_width: u32,
430    pub old_img_height: u32,
431    /// Scaling mode: 0=Fill, 1=Fit, 2=Stretch, 3=Center, 4=Tile.
432    pub scaling_mode: u32,
433}