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 padding2: 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            padding2: 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            padding2: 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    /// Get or create a pipeline for the given surface texture format.
193    fn get_pipeline(&self, format: wgpu::TextureFormat) -> wgpu::RenderPipeline {
194        let mut cache = self.pipeline.lock().unwrap();
195        if let Some((cached_fmt, ref pipeline)) = *cache
196            && cached_fmt == format
197        {
198            return pipeline.clone();
199        }
200
201        let pipeline = self
202            .device
203            .create_render_pipeline(&wgpu::RenderPipelineDescriptor {
204                label: Some("Render Pipeline"),
205                layout: Some(&self.pipeline_layout),
206                vertex: wgpu::VertexState {
207                    module: &self.shader,
208                    entry_point: Some("vs_main"),
209                    buffers: &[],
210                    compilation_options: wgpu::PipelineCompilationOptions::default(),
211                },
212                fragment: Some(wgpu::FragmentState {
213                    module: &self.shader,
214                    entry_point: Some("fs_main"),
215                    targets: &[Some(wgpu::ColorTargetState {
216                        format,
217                        blend: Some(wgpu::BlendState::REPLACE),
218                        write_mask: wgpu::ColorWrites::ALL,
219                    })],
220                    compilation_options: wgpu::PipelineCompilationOptions::default(),
221                }),
222                primitive: wgpu::PrimitiveState {
223                    topology: wgpu::PrimitiveTopology::TriangleList,
224                    strip_index_format: None,
225                    front_face: wgpu::FrontFace::Ccw,
226                    cull_mode: None, // Don't cull — fullscreen quad
227                    polygon_mode: wgpu::PolygonMode::Fill,
228                    unclipped_depth: false,
229                    conservative: false,
230                },
231                depth_stencil: None,
232                multisample: wgpu::MultisampleState {
233                    count: 1,
234                    mask: !0,
235                    alpha_to_coverage_enabled: false,
236                },
237                multiview: None,
238                cache: None,
239            });
240
241        *cache = Some((format, pipeline.clone()));
242        pipeline
243    }
244
245    /// Creates a reusable RGBA8 texture with a matching bind group. Live
246    /// wallpapers upload new frame data into it via `update_texture`.
247    pub fn create_texture(&self, width: u32, height: u32) -> (wgpu::Texture, wgpu::BindGroup) {
248        let size = wgpu::Extent3d {
249            width,
250            height,
251            depth_or_array_layers: 1,
252        };
253
254        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
255            label: None,
256            size,
257            mip_level_count: 1,
258            sample_count: 1,
259            dimension: wgpu::TextureDimension::D2,
260            format: wgpu::TextureFormat::Rgba8UnormSrgb,
261            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
262            view_formats: &[],
263        });
264
265        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
266        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
267            address_mode_u: wgpu::AddressMode::ClampToEdge,
268            address_mode_v: wgpu::AddressMode::ClampToEdge,
269            address_mode_w: wgpu::AddressMode::ClampToEdge,
270            mag_filter: wgpu::FilterMode::Linear,
271            min_filter: wgpu::FilterMode::Nearest,
272            mipmap_filter: wgpu::FilterMode::Nearest,
273            ..Default::default()
274        });
275
276        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
277            layout: &self.bind_group_layout_tex,
278            entries: &[
279                wgpu::BindGroupEntry {
280                    binding: 0,
281                    resource: wgpu::BindingResource::TextureView(&view),
282                },
283                wgpu::BindGroupEntry {
284                    binding: 1,
285                    resource: wgpu::BindingResource::Sampler(&sampler),
286                },
287            ],
288            label: None,
289        });
290
291        (texture, bind_group)
292    }
293
294    /// Replaces the contents of a texture created by `create_texture`.
295    pub fn update_texture(&self, texture: &wgpu::Texture, rgba: &[u8], width: u32, height: u32) {
296        self.queue.write_texture(
297            wgpu::TexelCopyTextureInfo {
298                texture,
299                mip_level: 0,
300                origin: wgpu::Origin3d::ZERO,
301                aspect: wgpu::TextureAspect::All,
302            },
303            rgba,
304            wgpu::TexelCopyBufferLayout {
305                offset: 0,
306                bytes_per_row: Some(4 * width),
307                rows_per_image: Some(height),
308            },
309            wgpu::Extent3d {
310                width,
311                height,
312                depth_or_array_layers: 1,
313            },
314        );
315    }
316
317    pub fn load_texture(
318        &self,
319        image: &image::DynamicImage,
320    ) -> anyhow::Result<(wgpu::Texture, wgpu::BindGroup)> {
321        let rgba = image.to_rgba8();
322        let (width, height) = image.dimensions();
323        let (texture, bind_group) = self.create_texture(width, height);
324        self.update_texture(&texture, &rgba, width, height);
325        Ok((texture, bind_group))
326    }
327
328    pub fn update_uniforms(&self, uniforms: Uniforms) {
329        self.queue
330            .write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
331    }
332
333    /// Renders the given texture bind groups to a wgpu Surface for one frame.
334    /// bg_bind = old wallpaper, new_bind = new wallpaper, transitioned by effect.
335    pub fn render_frame(&self, request: FrameRequest) -> anyhow::Result<FrameStatus> {
336        let FrameRequest {
337            surface,
338            format,
339            bg_bind,
340            new_bind,
341            effect,
342            width,
343            height,
344            img_width,
345            img_height,
346            old_img_width,
347            old_img_height,
348        } = request;
349        let mut uniforms = Uniforms::from_effect(effect);
350        uniforms.resolution = [width as f32, height as f32];
351        uniforms.image_resolution = [img_width as f32, img_height as f32];
352        uniforms.old_image_resolution = [old_img_width as f32, old_img_height as f32];
353        self.update_uniforms(uniforms);
354
355        let pipeline = self.get_pipeline(format);
356        // `get_current_texture` blocks until the compositor presents (Fifo),
357        // which paces rendering to the refresh rate. A stalled compositor
358        // parks this call, which is safe because transition loops always run
359        // on detached tasks that never block the daemon's IPC loop.
360        let output = match surface.get_current_texture() {
361            Ok(texture) => texture,
362            Err(wgpu::SurfaceError::Timeout) => return Ok(FrameStatus::TimedOut),
363            Err(err) => {
364                return Err(anyhow::anyhow!(
365                    "failed to acquire swapchain texture: {err:?}"
366                ));
367            }
368        };
369        let view = output
370            .texture
371            .create_view(&wgpu::TextureViewDescriptor::default());
372
373        let mut encoder = self
374            .device
375            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
376                label: Some("Render Encoder"),
377            });
378
379        {
380            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
381                label: Some("Wallpaper Render Pass"),
382                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
383                    view: &view,
384                    resolve_target: None,
385                    ops: wgpu::Operations {
386                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
387                        store: wgpu::StoreOp::Store,
388                    },
389                })],
390                depth_stencil_attachment: None,
391                occlusion_query_set: None,
392                timestamp_writes: None,
393            });
394
395            render_pass.set_pipeline(&pipeline);
396            render_pass.set_bind_group(0, bg_bind, &[]);
397            render_pass.set_bind_group(1, new_bind, &[]);
398            render_pass.set_bind_group(2, &self.uniform_bind_group, &[]);
399            // The vertex shader generates a fullscreen triangle from vertex_index 0..3
400            render_pass.draw(0..3, 0..1);
401        }
402
403        self.queue.submit(std::iter::once(encoder.finish()));
404        output.present();
405
406        Ok(FrameStatus::Presented)
407    }
408}
409
410/// Whether a frame was actually presented to the surface. A `TimedOut` frame
411/// means the compositor is not requesting frames right now (e.g. the monitor
412/// is off), and callers should stop rendering instead of spinning.
413#[derive(Debug, Clone, Copy, PartialEq, Eq)]
414pub enum FrameStatus {
415    Presented,
416    TimedOut,
417}
418
419/// Everything needed to present one transition frame to a surface.
420pub struct FrameRequest<'a> {
421    pub surface: &'a wgpu::Surface<'a>,
422    pub format: wgpu::TextureFormat,
423    /// Outgoing wallpaper frame, normally the daemon's previous wallpaper.
424    pub bg_bind: &'a wgpu::BindGroup,
425    /// New wallpaper.
426    pub new_bind: &'a wgpu::BindGroup,
427    /// Effect uniforms for this frame (progress, params, origin, easing...).
428    pub effect: &'a crate::animation::EffectUniforms,
429    pub width: u32,
430    pub height: u32,
431    pub img_width: u32,
432    pub img_height: u32,
433    pub old_img_width: u32,
434    pub old_img_height: u32,
435}