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