Skip to main content

wallr_core/daemon/
mod.rs

1use crate::config::WallrConfig;
2use crate::ipc::{IpcCommand, IpcResponse, start_ipc_server};
3use crate::renderer::Renderer;
4use crate::wallpaper::{SetOptions, WallpaperEngine};
5use notify::{Event, EventKind, RecursiveMode, Watcher};
6use std::path::PathBuf;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, Ordering};
9use tokio::sync::Mutex;
10
11use raw_window_handle::{
12    DisplayHandle, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle,
13    WaylandDisplayHandle, WaylandWindowHandle, WindowHandle,
14};
15use smithay_client_toolkit::{
16    compositor::{CompositorHandler, CompositorState},
17    delegate_compositor, delegate_layer, delegate_output, delegate_registry, delegate_shm,
18    output::{OutputHandler, OutputState},
19    registry::{ProvidesRegistryState, RegistryState},
20    registry_handlers,
21    shell::WaylandSurface,
22    shell::wlr_layer::{
23        Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
24        LayerSurfaceConfigure,
25    },
26    shm::{Shm, ShmHandler},
27};
28use wayland_client::{
29    Connection, Proxy, QueueHandle,
30    globals::registry_queue_init,
31    protocol::{wl_compositor, wl_output, wl_surface},
32};
33#[derive(Debug, thiserror::Error)]
34pub enum DaemonError {
35    #[error("daemon already running: {0}")]
36    AlreadyRunning(String),
37    #[error("failed to start daemon: {0}")]
38    StartError(String),
39    #[error("I/O error: {0}")]
40    Io(#[from] std::io::Error),
41    #[error("IPC error: {0}")]
42    Ipc(#[from] crate::ipc::IpcError),
43    #[error("Config error: {0}")]
44    Config(#[from] crate::config::ConfigError),
45    #[error("Wallpaper error: {0}")]
46    Wallpaper(#[from] crate::wallpaper::WallpaperError),
47}
48
49pub struct WaylandWindow {
50    pub display: *mut std::ffi::c_void,
51    pub surface: *mut std::ffi::c_void,
52}
53
54unsafe impl Send for WaylandWindow {}
55unsafe impl Sync for WaylandWindow {}
56
57impl HasWindowHandle for WaylandWindow {
58    fn window_handle(&self) -> Result<WindowHandle<'_>, raw_window_handle::HandleError> {
59        let surface = std::ptr::NonNull::new(self.surface)
60            .ok_or(raw_window_handle::HandleError::Unavailable)?;
61        let handle = WaylandWindowHandle::new(surface);
62        unsafe { Ok(WindowHandle::borrow_raw(RawWindowHandle::Wayland(handle))) }
63    }
64}
65
66impl HasDisplayHandle for WaylandWindow {
67    fn display_handle(&self) -> Result<DisplayHandle<'_>, raw_window_handle::HandleError> {
68        let display = std::ptr::NonNull::new(self.display)
69            .ok_or(raw_window_handle::HandleError::Unavailable)?;
70        let handle = WaylandDisplayHandle::new(display);
71        unsafe { Ok(DisplayHandle::borrow_raw(RawDisplayHandle::Wayland(handle))) }
72    }
73}
74
75struct OutputInfo {
76    name: String,
77    width: u32,
78    height: u32,
79    scale_factor: i32,
80    wl_output: wl_output::WlOutput,
81}
82
83struct WaylandState {
84    registry_state: RegistryState,
85    output_state: OutputState,
86    compositor_state: CompositorState,
87    shm: Shm,
88    outputs: std::collections::HashMap<u32, OutputInfo>,
89    surfaces: Vec<(u32, LayerSurface)>,
90}
91
92impl ProvidesRegistryState for WaylandState {
93    fn registry(&mut self) -> &mut RegistryState {
94        &mut self.registry_state
95    }
96
97    registry_handlers![OutputState,];
98}
99
100impl CompositorHandler for WaylandState {
101    fn scale_factor_changed(
102        &mut self,
103        _conn: &Connection,
104        _qh: &QueueHandle<Self>,
105        _surface: &wl_surface::WlSurface,
106        _new_factor: i32,
107    ) {
108    }
109    fn transform_changed(
110        &mut self,
111        _conn: &Connection,
112        _qh: &QueueHandle<Self>,
113        _surface: &wl_surface::WlSurface,
114        _new_transform: wl_output::Transform,
115    ) {
116    }
117    fn frame(
118        &mut self,
119        _conn: &Connection,
120        _qh: &QueueHandle<Self>,
121        _surface: &wl_surface::WlSurface,
122        _time: u32,
123    ) {
124    }
125    fn surface_enter(
126        &mut self,
127        _conn: &Connection,
128        _qh: &QueueHandle<Self>,
129        _surface: &wl_surface::WlSurface,
130        _output: &wl_output::WlOutput,
131    ) {
132    }
133    fn surface_leave(
134        &mut self,
135        _conn: &Connection,
136        _qh: &QueueHandle<Self>,
137        _surface: &wl_surface::WlSurface,
138        _output: &wl_output::WlOutput,
139    ) {
140    }
141}
142
143impl wayland_client::Dispatch<wayland_client::protocol::wl_region::WlRegion, ()> for WaylandState {
144    fn event(
145        _state: &mut WaylandState,
146        _region: &wayland_client::protocol::wl_region::WlRegion,
147        _event: wayland_client::protocol::wl_region::Event,
148        _data: &(),
149        _conn: &Connection,
150        _qh: &QueueHandle<WaylandState>,
151    ) {
152    }
153}
154
155impl LayerShellHandler for WaylandState {
156    fn configure(
157        &mut self,
158        _conn: &Connection,
159        _qh: &QueueHandle<Self>,
160        layer: &LayerSurface,
161        _configure: LayerSurfaceConfigure,
162        _serial: u32,
163    ) {
164        layer.commit();
165    }
166
167    fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {}
168}
169
170impl ShmHandler for WaylandState {
171    fn shm_state(&mut self) -> &mut Shm {
172        &mut self.shm
173    }
174}
175
176impl OutputHandler for WaylandState {
177    fn output_state(&mut self) -> &mut OutputState {
178        &mut self.output_state
179    }
180    fn new_output(
181        &mut self,
182        _conn: &Connection,
183        _qh: &QueueHandle<Self>,
184        output: wl_output::WlOutput,
185    ) {
186        let id = output.id().protocol_id();
187        let info = OutputInfo {
188            name: format!("output-{id}"),
189            width: 1920,
190            height: 1080,
191            scale_factor: 1,
192            wl_output: output,
193        };
194        self.outputs.insert(id, info);
195    }
196    fn update_output(
197        &mut self,
198        _conn: &Connection,
199        _qh: &QueueHandle<Self>,
200        output: wl_output::WlOutput,
201    ) {
202        let id = output.id().protocol_id();
203        if let Some(info) = self.outputs.get_mut(&id) {
204            if let Some(mode) = self
205                .output_state
206                .info(&output)
207                .and_then(|i| i.modes.iter().find(|m| m.current).cloned())
208            {
209                info.width = mode.dimensions.0 as u32;
210                info.height = mode.dimensions.1 as u32;
211            }
212            if let Some(info_data) = self.output_state.info(&output) {
213                info.scale_factor = info_data.scale_factor;
214                if !info.name.starts_with("output-") {
215                    info.name = info_data.name.clone().unwrap_or_else(|| info.name.clone());
216                }
217            }
218        }
219    }
220    fn output_destroyed(
221        &mut self,
222        _conn: &Connection,
223        _qh: &QueueHandle<Self>,
224        output: wl_output::WlOutput,
225    ) {
226        let id = output.id().protocol_id();
227        self.outputs.remove(&id);
228    }
229}
230
231delegate_compositor!(WaylandState);
232delegate_layer!(WaylandState);
233delegate_output!(WaylandState);
234delegate_registry!(WaylandState);
235delegate_shm!(WaylandState);
236
237/// Wakes paced live-playback loops when a new commit bumps the generation.
238struct LivePacer {
239    lock: std::sync::Mutex<()>,
240    cond: std::sync::Condvar,
241}
242
243impl LivePacer {
244    fn new() -> Self {
245        Self {
246            lock: std::sync::Mutex::new(()),
247            cond: std::sync::Condvar::new(),
248        }
249    }
250
251    fn notify(&self) {
252        let _guard = self.lock.lock().unwrap();
253        self.cond.notify_all();
254    }
255
256    /// Blocks until `deadline` or until `notify` is called, whichever comes
257    /// first.
258    fn wait_until(&self, deadline: std::time::Instant) {
259        let guard = self.lock.lock().unwrap();
260        let now = std::time::Instant::now();
261        if deadline <= now {
262            return;
263        }
264        let _ = self
265            .cond
266            .wait_timeout_while(guard, deadline - now, |_| true);
267    }
268}
269
270struct RenderState {
271    renderer: std::sync::Arc<Renderer>,
272    surface: &'static wgpu::Surface<'static>,
273    /// Serializes transition rendering. The lock is only ever held by the
274    /// detached render task, never by the IPC loop, so a stalled present
275    /// cannot freeze the daemon.
276    render_lock: std::sync::Arc<std::sync::Mutex<()>>,
277    /// Bumped on every commit. Live playback checks it each frame and stops
278    /// as soon as a new wallpaper supersedes the one it is playing.
279    playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
280    /// Wakes paced live-playback loops when a new commit bumps the generation,
281    /// so an old player exits immediately instead of after its sleep quantum.
282    pacer: std::sync::Arc<LivePacer>,
283    current_bind: Option<wgpu::BindGroup>,
284    current_tex: Option<wgpu::Texture>,
285    width: u32,
286    height: u32,
287    current_width: u32,
288    current_height: u32,
289    format: wgpu::TextureFormat,
290    /// Video playback manager
291    video_playback: std::sync::Arc<crate::video::VideoPlayback>,
292    /// Hardware backend to request for new decoders (from `video.hw_decode`).
293    hw_accel: crate::video::HwAccel,
294    /// Current scaling mode for live playback.
295    scaling_mode: u32,
296}
297
298/// Everything the transition render task needs; the daemon state has already
299/// been promoted to the new wallpaper before a transition is spawned.
300struct CommitData {
301    bg_bind: wgpu::BindGroup,
302    new_bind: wgpu::BindGroup,
303    img_width: u32,
304    img_height: u32,
305    old_img_width: u32,
306    old_img_height: u32,
307    format: wgpu::TextureFormat,
308    width: u32,
309    height: u32,
310    /// Animated frames to play live after the transition, when the committed
311    /// file is a GIF.
312    animated: Option<crate::animated::AnimatedImage>,
313    /// Video metadata when committed file is a video.
314    is_video: bool,
315    /// Playback generation captured at commit time; live playback stops when
316    /// it no longer matches `RenderState::playback_gen`.
317    generation: u64,
318    /// Scaling mode: 0=Fill, 1=Fit, 2=Stretch, 3=Center, 4=Tile.
319    scaling_mode: u32,
320}
321
322impl RenderState {
323    async fn set_wallpaper(
324        &mut self,
325        path: &std::path::Path,
326        effect: &crate::animation::Effect,
327        duration_ms: u32,
328        scaling_mode: u32,
329    ) -> anyhow::Result<()> {
330        self.scaling_mode = scaling_mode;
331        let commit = self.commit_wallpaper(path, scaling_mode)?;
332        self.spawn_transition(commit, effect, duration_ms);
333        Ok(())
334    }
335
336    /// Loads the new wallpaper and atomically promotes it to the current
337    /// frame. The outgoing bind group stays alive for the transition, so the
338    /// render task can keep drawing from it after this commit returns.
339    fn commit_wallpaper(
340        &mut self,
341        path: &std::path::Path,
342        scaling_mode: u32,
343    ) -> anyhow::Result<CommitData> {
344        use image::ImageReader;
345
346        // Check if this is a video file FIRST
347        if crate::video::VideoDecoder::is_video_file(path) {
348            tracing::info!("Video file detected: {:?}", path);
349
350            // Invalidate any in-flight video render task BEFORE touching the
351            // shared decoder: the old task checks the generation on every
352            // iteration, so bumping first makes it exit (or skip the upload)
353            // before it could pull a frame of the new resolution from the
354            // freshly started decoder.
355            let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
356            self.pacer.notify();
357
358            // Start video playback (replaces any previous playback and joins
359            // its decode thread, releasing the old decoder's buffers).
360            let metadata = self.video_playback.start(path, self.hw_accel)?;
361
362            // Wait for the first frame so the transition's incoming image is
363            // the real first frame, not a black placeholder.
364            let first_frame = self
365                .video_playback
366                .wait_first_frame(std::time::Duration::from_millis(1000));
367
368            let (new_tex, new_bind, img_width, img_height) = if let Some(frame) = first_frame {
369                let (tex, bind) = self.renderer.create_texture(frame.width, frame.height);
370                self.renderer
371                    .update_texture(&tex, &frame.data, frame.width, frame.height);
372                (tex, bind, frame.width, frame.height)
373            } else {
374                // Fallback: create black texture
375                tracing::warn!("No first frame available, using black texture");
376                let (tex, bind) = self
377                    .renderer
378                    .create_texture(metadata.width, metadata.height);
379                let black = vec![0u8; (metadata.width * metadata.height * 4) as usize];
380                self.renderer
381                    .update_texture(&tex, &black, metadata.width, metadata.height);
382                (tex, bind, metadata.width, metadata.height)
383            };
384
385            let old_bind = self.current_bind.take();
386            let (old_img_width, old_img_height) = if old_bind.is_some() {
387                (self.current_width.max(1), self.current_height.max(1))
388            } else {
389                (img_width, img_height)
390            };
391            let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
392
393            drop(self.current_tex.take());
394            self.current_tex = Some(new_tex);
395            self.current_bind = Some(new_bind.clone());
396            self.current_width = img_width;
397            self.current_height = img_height;
398
399            return Ok(CommitData {
400                bg_bind,
401                new_bind,
402                img_width,
403                img_height,
404                old_img_width,
405                old_img_height,
406                format: self.format,
407                width: self.width,
408                height: self.height,
409                animated: None,
410                is_video: true,
411                generation,
412                scaling_mode,
413            });
414        }
415
416        // A static image or GIF supersedes any video: release the video
417        // decoder and its buffers immediately (the generation bump also stops
418        // the video render task on its next vsync).
419        self.video_playback.stop();
420
421        // Stream animated frames (GIF) on demand during playback; the
422        // transition's incoming texture is the GIF's first frame.
423        let mut animated = crate::animated::AnimatedImage::decode(path)?;
424        let (new_tex, new_bind, img_width, img_height) = if let Some(anim) = animated.as_mut() {
425            let (w, h) = (anim.width, anim.height);
426            let (tex, bind) = self.renderer.create_texture(w, h);
427            let first = anim.first_frame();
428            if !first.is_empty() {
429                self.renderer.update_texture(&tex, first, w, h);
430            }
431            (tex, bind, w, h)
432        } else {
433            let new_img = ImageReader::open(path)?.decode()?;
434            let (tex, bind) = self.renderer.load_texture(&new_img)?;
435            (tex, bind, new_img.width(), new_img.height())
436        };
437
438        let old_bind = self.current_bind.take();
439        let (old_img_width, old_img_height) = if old_bind.is_some() {
440            (self.current_width.max(1), self.current_height.max(1))
441        } else {
442            (img_width, img_height)
443        };
444        // Keep the last image as the outgoing frame. On the first ever run,
445        // using the incoming image for both sides is a clean no-op transition;
446        // it avoids a black flash while still allowing the cached wallpaper
447        // restored at daemon startup to become the real outgoing frame.
448        let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
449
450        drop(self.current_tex.take());
451        self.current_tex = Some(new_tex);
452        self.current_bind = Some(new_bind.clone());
453        self.current_width = img_width;
454        self.current_height = img_height;
455
456        let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
457        self.pacer.notify();
458
459        Ok(CommitData {
460            bg_bind,
461            new_bind,
462            img_width,
463            img_height,
464            old_img_width,
465            old_img_height,
466            format: self.format,
467            width: self.width,
468            height: self.height,
469            animated,
470            is_video: false,
471            generation,
472            scaling_mode,
473        })
474    }
475
476    /// Renders the committed transition on a detached blocking task. The IPC
477    /// path never waits on GPU presents, so a stalled compositor (monitor
478    /// off, suspend) cannot hang the daemon. Transitions are serialized by
479    /// the render lock: a later one simply waits until the earlier drains.
480    fn spawn_transition(
481        &self,
482        commit: CommitData,
483        effect: &crate::animation::Effect,
484        duration_ms: u32,
485    ) {
486        let renderer = self.renderer.clone();
487        let surface: &'static wgpu::Surface<'static> = self.surface;
488        let render_lock = self.render_lock.clone();
489        let playback_gen = self.playback_gen.clone();
490        let pacer = self.pacer.clone();
491        let video_playback = self.video_playback.clone();
492        let effect = effect.clone();
493        drop(tokio::task::spawn_blocking(move || {
494            render_transition(
495                renderer,
496                surface,
497                render_lock,
498                playback_gen,
499                pacer,
500                video_playback,
501                commit,
502                effect,
503                duration_ms,
504            );
505        }));
506    }
507}
508
509/// Presents one frame per vsync until the wall-clock duration elapses. With
510/// PresentMode::Fifo, `get_current_texture` blocks until the previous frame
511/// is presented, so this loop is paced to the monitor refresh rate, and the
512/// transition lasts exactly `duration_ms` on any refresh rate — frame-count
513/// pacing would run too fast on high-refresh panels and too slow when the
514/// present rate is low. If the compositor stops presenting, the loop can park
515/// inside a present; that is fine here because the task is detached.
516#[allow(clippy::too_many_arguments)]
517fn render_transition(
518    renderer: std::sync::Arc<Renderer>,
519    surface: &'static wgpu::Surface<'static>,
520    render_lock: std::sync::Arc<std::sync::Mutex<()>>,
521    playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
522    pacer: std::sync::Arc<LivePacer>,
523    video_playback: std::sync::Arc<crate::video::VideoPlayback>,
524    mut commit: CommitData,
525    effect: crate::animation::Effect,
526    duration_ms: u32,
527) {
528    let _guard = render_lock
529        .lock()
530        .unwrap_or_else(|poisoned| poisoned.into_inner());
531
532    let duration = std::time::Duration::from_millis(u64::from(duration_ms.max(1)));
533    let start = std::time::Instant::now();
534    loop {
535        let progress = start.elapsed().as_secs_f32() / duration.as_secs_f32();
536        let uniforms = crate::animation::compute_effect_uniforms(&effect, progress.clamp(0.0, 1.0));
537        let status = renderer.render_frame(crate::renderer::FrameRequest {
538            surface,
539            format: commit.format,
540            bg_bind: &commit.bg_bind,
541            new_bind: &commit.new_bind,
542            effect: &uniforms,
543            width: commit.width,
544            height: commit.height,
545            img_width: commit.img_width,
546            img_height: commit.img_height,
547            old_img_width: commit.old_img_width,
548            old_img_height: commit.old_img_height,
549            scaling_mode: commit.scaling_mode,
550        });
551        let status = match status {
552            Ok(status) => status,
553            Err(err) => {
554                eprintln!("wallr: transition render failed: {err}");
555                break;
556            }
557        };
558        if progress >= 1.0 || status == crate::renderer::FrameStatus::TimedOut {
559            break;
560        }
561    }
562
563    // The transition ended; if the committed wallpaper is an animated GIF and
564    // nothing superseded it while we rendered, keep the render lock and play
565    // the frames live until the next commit bumps the generation.
566    let mut animated = commit.animated.take();
567    if let Some(animated) = animated.as_mut()
568        && playback_gen.load(Ordering::SeqCst) == commit.generation
569    {
570        play_live(&renderer, surface, &commit, animated, &playback_gen, &pacer);
571    } else if commit.is_video && playback_gen.load(Ordering::SeqCst) == commit.generation {
572        play_video(&renderer, surface, &commit, &video_playback, &playback_gen);
573    }
574}
575
576/// Presents live wallpaper frames until the next commit. One frame is
577/// presented per GIF frame boundary instead of at the monitor refresh rate.
578/// Two textures are double-buffered and frames are decompressed directly
579/// into a mapped staging ring (no intermediate copy), so the wake path only
580/// presents and the pacing sleep hides the decode/upload entirely.
581fn play_live(
582    renderer: &Renderer,
583    surface: &'static wgpu::Surface<'static>,
584    commit: &CommitData,
585    animated: &mut crate::animated::AnimatedImage,
586    playback_gen: &std::sync::atomic::AtomicU64,
587    pacer: &LivePacer,
588) {
589    let (tex_a, bind_a) = renderer.create_texture(animated.width, animated.height);
590    let (tex_b, bind_b) = renderer.create_texture(animated.width, animated.height);
591    let (frame_w, frame_h) = (animated.width, animated.height);
592    let (bytes_per_row, rows) = (frame_w * 4, frame_h);
593    let frame_bytes = bytes_per_row as u64 * rows as u64;
594
595    // Map+decompress+copy path needs a byte-per-row multiple of the copy
596    // alignment; fall back to write_texture for odd widths.
597    let direct_upload = bytes_per_row % 256 == 0;
598    let staging: Vec<wgpu::Buffer> = if direct_upload {
599        (0..2)
600            .map(|_| {
601                renderer.device.create_buffer(&wgpu::BufferDescriptor {
602                    label: Some("wallr-gif-staging"),
603                    size: frame_bytes,
604                    usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
605                    mapped_at_creation: false,
606                })
607            })
608            .collect()
609    } else {
610        Vec::new()
611    };
612
613    let first = animated.first_frame();
614    if !first.is_empty() {
615        renderer.update_texture(&tex_a, first, frame_w, frame_h);
616        renderer.update_texture(&tex_b, first, frame_w, frame_h);
617    }
618    let binds = [bind_a, bind_b];
619    let textures = [tex_a, tex_b];
620
621    // Uploads frame `index` into `textures[tgt]`. Returns true when the GPU
622    // copy was recorded.
623    let upload = |renderer: &Renderer,
624                  tgt: usize,
625                  index: usize,
626                  slot: usize,
627                  animated: &mut crate::animated::AnimatedImage|
628     -> bool {
629        if direct_upload {
630            let buffer = &staging[slot];
631            let slice = buffer.slice(..);
632            slice.map_async(wgpu::MapMode::Write, |_| {});
633            renderer.device.poll(wgpu::Maintain::Wait);
634            let ok = {
635                let mut mapped = slice.get_mapped_range_mut();
636                animated.decompress_into(index, &mut mapped)
637            };
638            buffer.unmap();
639            if ok {
640                let mut encoder = renderer
641                    .device
642                    .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
643                encoder.copy_buffer_to_texture(
644                    wgpu::TexelCopyBufferInfo {
645                        buffer,
646                        layout: wgpu::TexelCopyBufferLayout {
647                            offset: 0,
648                            bytes_per_row: Some(bytes_per_row),
649                            rows_per_image: Some(rows),
650                        },
651                    },
652                    wgpu::TexelCopyTextureInfo {
653                        texture: &textures[tgt],
654                        mip_level: 0,
655                        origin: wgpu::Origin3d::ZERO,
656                        aspect: wgpu::TextureAspect::All,
657                    },
658                    wgpu::Extent3d {
659                        width: frame_w,
660                        height: frame_h,
661                        depth_or_array_layers: 1,
662                    },
663                );
664                renderer.queue.submit([encoder.finish()]);
665                return true;
666            }
667        } else {
668            let frame = animated.frame_at(index);
669            if !frame.is_empty() {
670                renderer.update_texture(&textures[tgt], frame, frame_w, frame_h);
671                return true;
672            }
673        }
674        false
675    };
676
677    let mut cur = 0usize; // texture index currently holding the presented frame
678    let mut cur_frame = 0usize; // frame index currently in texture `cur`
679    let mut next_frame = 0usize; // frame index currently in the idle texture
680    let mut slot = 0usize; // staging ring slot for the next upload
681    let start = std::time::Instant::now();
682    let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
683    loop {
684        if playback_gen.load(Ordering::SeqCst) != commit.generation {
685            return;
686        }
687        let index = animated.frame_index_at(start.elapsed());
688        if index != cur_frame {
689            if next_frame != index {
690                upload(renderer, cur ^ 1, index, slot, animated);
691                slot ^= 1;
692                next_frame = index;
693            }
694            cur ^= 1;
695            cur_frame = index;
696        }
697        let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
698        let status = renderer.render_frame(crate::renderer::FrameRequest {
699            surface,
700            format: commit.format,
701            bg_bind: &binds[cur],
702            new_bind: &binds[cur],
703            effect: &uniforms,
704            width: commit.width,
705            height: commit.height,
706            img_width: animated.width,
707            img_height: animated.height,
708            old_img_width: animated.width,
709            old_img_height: animated.height,
710            scaling_mode: commit.scaling_mode,
711        });
712        match status {
713            Ok(crate::renderer::FrameStatus::Presented) => {}
714            // A stalled present parks inside the acquire; a Timeout or error
715            // means the surface is unusable, so give up and let the next
716            // transition take over.
717            _ => return,
718        }
719
720        // Pace to the next GIF frame boundary instead of presenting at the
721        // monitor refresh rate: an animated wallpaper only needs a present
722        // when its frame changes. A commit wakes us via the pacer. The
723        // boundary is computed in absolute time (frame_start is loop-relative,
724        // so add the completed loops) to stay correct after the animation
725        // wraps. While waiting, warm the idle texture with the next frame so
726        // the wake path stays on the hot critical section.
727        let elapsed = start.elapsed();
728        let total: std::time::Duration = animated.total_duration();
729        let loops = (elapsed.as_millis() / total.as_millis().max(1)) as u64;
730        let next_change = animated.frame_start(index + 1) + total * (loops as u32);
731        let wait = next_change.saturating_sub(elapsed);
732        if wait > std::time::Duration::ZERO {
733            let next = index + 1;
734            if next_frame != next {
735                upload(renderer, cur ^ 1, next, slot, animated);
736                slot ^= 1;
737                next_frame = next;
738            }
739            pacer.wait_until(std::time::Instant::now() + wait);
740        }
741    }
742}
743
744/// Live video playback loop: continuously updates texture with decoded frames.
745fn play_video(
746    renderer: &Renderer,
747    surface: &'static wgpu::Surface<'static>,
748    commit: &CommitData,
749    video_playback: &std::sync::Arc<crate::video::VideoPlayback>,
750    playback_gen: &std::sync::atomic::AtomicU64,
751) {
752    // Get initial frame dimensions
753    let (width, height) = match video_playback.metadata() {
754        Some(meta) => (meta.width, meta.height),
755        None => {
756            tracing::warn!("No video metadata available");
757            return;
758        }
759    };
760
761    let (texture, bind) = renderer.create_texture(width, height);
762    let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
763
764    // The texture starts empty; present a real frame before the first
765    // vsync so the surface never flashes black.
766    let mut uploaded = false;
767
768    loop {
769        // A newer commit superseded us. Do NOT touch the shared
770        // `video_playback` here: the successor commit already replaced the
771        // decoder (video) or stopped it (static image), and stopping it now
772        // would kill the successor's playback too.
773        if playback_gen.load(Ordering::SeqCst) != commit.generation {
774            return;
775        }
776
777        // Pull the next displayable frame. The decoder queue is bounded, so
778        // this never blocks; `None` means "present the current texture".
779        if let Some(frame) = video_playback.next_frame() {
780            // The shared decoder can be replaced between commits; never
781            // upload a frame whose size does not match this task's texture.
782            if frame.width != width || frame.height != height {
783                continue;
784            }
785            renderer.update_texture(&texture, &frame.data, frame.width, frame.height);
786            uploaded = true;
787        }
788
789        if !uploaded {
790            // No frame yet; wait briefly and try again instead of presenting
791            // an uninitialized texture.
792            std::thread::sleep(std::time::Duration::from_millis(2));
793            continue;
794        }
795
796        let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
797        let status = renderer.render_frame(crate::renderer::FrameRequest {
798            surface,
799            format: commit.format,
800            bg_bind: &bind,
801            new_bind: &bind,
802            effect: &uniforms,
803            width: commit.width,
804            height: commit.height,
805            img_width: width,
806            img_height: height,
807            old_img_width: width,
808            old_img_height: height,
809            scaling_mode: commit.scaling_mode,
810        });
811
812        match status {
813            Ok(crate::renderer::FrameStatus::Presented) => {}
814            // A stalled present parks inside the acquire; a Timeout or error
815            // means the surface is unusable, so give up.
816            other => {
817                tracing::warn!("Video present failed ({:?}), stopping playback", other);
818                video_playback.stop();
819                return;
820            }
821        }
822    }
823}
824
825pub struct Daemon {
826    config: WallrConfig,
827    paused: Arc<AtomicBool>,
828    engine: Arc<Mutex<WallpaperEngine>>,
829}
830
831impl Daemon {
832    pub fn new(config: WallrConfig) -> Result<Self, DaemonError> {
833        let engine = WallpaperEngine::new(config.clone())?;
834        Ok(Self {
835            config,
836            paused: Arc::new(AtomicBool::new(false)),
837            engine: Arc::new(Mutex::new(engine)),
838        })
839    }
840
841    pub async fn start(self) -> Result<(), DaemonError> {
842        let socket_path = crate::config::expand_path(&self.config.daemon.socket);
843        if socket_path.exists() {
844            if tokio::net::UnixStream::connect(&socket_path).await.is_ok() {
845                return Err(DaemonError::AlreadyRunning(
846                    socket_path.to_string_lossy().to_string(),
847                ));
848            }
849            let _ = std::fs::remove_file(&socket_path);
850        }
851
852        let renderer = Renderer::new()
853            .await
854            .map_err(|e| DaemonError::StartError(format!("GPU init failed: {e}")))?;
855
856        let conn = Connection::connect_to_env()
857            .map_err(|e| DaemonError::StartError(format!("Failed to connect to Wayland: {e:?}")))?;
858        let backend = conn.backend();
859        let display_ptr = backend.display_ptr() as *mut std::ffi::c_void;
860
861        let (globals, mut event_queue) = registry_queue_init(&conn)
862            .map_err(|e| DaemonError::StartError(format!("registry_queue_init failed: {e:?}")))?;
863        let qh = event_queue.handle();
864
865        let compositor_state = CompositorState::bind(&globals, &qh)
866            .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
867        let layer_shell = LayerShell::bind(&globals, &qh)
868            .map_err(|e| DaemonError::StartError(format!("layer_shell bind failed: {e:?}")))?;
869        let shm = Shm::bind(&globals, &qh)
870            .map_err(|e| DaemonError::StartError(format!("shm bind failed: {e:?}")))?;
871
872        let mut wayland_state = WaylandState {
873            registry_state: RegistryState::new(&globals),
874            output_state: OutputState::new(&globals, &qh),
875            compositor_state,
876            shm,
877            outputs: std::collections::HashMap::new(),
878            surfaces: Vec::new(),
879        };
880
881        // Bind compositor once for creating empty input regions (passthrough).
882        let compositor = globals
883            .bind::<wl_compositor::WlCompositor, WaylandState, smithay_client_toolkit::globals::GlobalData>(
884                &qh,
885                1..=4,
886                smithay_client_toolkit::globals::GlobalData,
887            )
888            .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
889
890        // Two roundtrips: first discovers outputs, second gets their modes/scale.
891        event_queue
892            .roundtrip(&mut wayland_state)
893            .map_err(|e| DaemonError::StartError(format!("roundtrip failed: {e:?}")))?;
894        event_queue
895            .roundtrip(&mut wayland_state)
896            .map_err(|e| DaemonError::StartError(format!("roundtrip2 failed: {e:?}")))?;
897
898        if wayland_state.outputs.is_empty() {
899            return Err(DaemonError::StartError(
900                "no outputs detected after roundtrip".into(),
901            ));
902        }
903
904        let renderer = std::sync::Arc::new(renderer);
905
906        // Create a LayerSurface, wgpu Surface, and RenderState for every
907        // known output. The key is the output's human-readable name (e.g.
908        // "DP-1", "eDP-1") so IPC can target a specific monitor.
909        let mut render_states: std::collections::HashMap<String, Arc<Mutex<RenderState>>> =
910            std::collections::HashMap::new();
911
912        // Collect output info first so we can pass &mut wayland_state to the
913        // helper (we need &mut to push LayerSurfaces into the surfaces vec).
914        let output_info: Vec<(u32, OutputInfo)> = wayland_state
915            .outputs
916            .iter()
917            .map(|(k, v)| {
918                (
919                    *k,
920                    OutputInfo {
921                        name: v.name.clone(),
922                        width: v.width,
923                        height: v.height,
924                        scale_factor: v.scale_factor,
925                        wl_output: v.wl_output.clone(),
926                    },
927                )
928            })
929            .collect();
930
931        for (proto_id, info) in &output_info {
932            let name = info.name.clone();
933            let rs = Self::create_render_state_for_output(
934                &renderer,
935                display_ptr,
936                &mut wayland_state,
937                &qh,
938                &layer_shell,
939                &compositor,
940                info,
941                &self.config,
942            )
943            .await?;
944            let rs = Arc::new(Mutex::new(rs));
945            render_states.insert(name.clone(), rs.clone());
946
947            // Restore per-output last wallpaper.
948            let state_path = dirs::cache_dir()
949                .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
950                .join(format!("wallr/last_wallpaper/{name}"));
951            if let Ok(path_str) = std::fs::read_to_string(&state_path) {
952                let p = std::path::Path::new(path_str.trim());
953                if p.exists() {
954                    let mut lock = rs.lock().await;
955                    let effect =
956                        crate::animation::Effect::Fade(crate::animation::FadeParams::default());
957                    let _ = lock.set_wallpaper(p, &effect, 0, 0).await;
958                }
959            }
960
961            tracing::info!("Output ready: {name} ({proto_id})");
962        }
963
964        let paused_clone = self.paused.clone();
965        let engine_clone = self.engine.clone();
966        let render_states_clone = render_states.clone();
967
968        // Graceful shutdown on POSIX signals: stop video decoding, remove the
969        // IPC socket, and exit. The compositor releases the layer-shell
970        // surface automatically when the process exits.
971        {
972            let rs_map = render_states.clone();
973            let socket_path = socket_path.clone();
974            tokio::spawn(async move {
975                use tokio::signal::unix::{SignalKind, signal};
976                let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler");
977                let mut int = signal(SignalKind::interrupt()).expect("SIGINT handler");
978                let mut hup = signal(SignalKind::hangup()).expect("SIGHUP handler");
979                tokio::select! {
980                    _ = term.recv() => {}
981                    _ = int.recv() => {}
982                    _ = hup.recv() => {}
983                }
984                tracing::info!("Signal received, shutting down gracefully");
985                for rs in rs_map.values() {
986                    if let Ok(state) = rs.try_lock() {
987                        state.video_playback.stop();
988                    }
989                }
990                let _ = std::fs::remove_file(&socket_path);
991                std::process::exit(0);
992            });
993        }
994
995        let ipc_socket_path = socket_path.clone();
996        start_ipc_server(&socket_path, move |cmd| {
997            let paused = paused_clone.clone();
998            let engine = engine_clone.clone();
999            let render_states = render_states_clone.clone();
1000            let stop_socket = ipc_socket_path.clone();
1001            async move {
1002                match cmd {
1003                    IpcCommand::Pause => {
1004                        paused.store(true, Ordering::SeqCst);
1005                        for rs in render_states.values() {
1006                            let rs_lock = rs.lock().await;
1007                            rs_lock.video_playback.pause();
1008                        }
1009                        IpcResponse {
1010                            success: true,
1011                            message: Some("Paused".into()),
1012                        }
1013                    }
1014                    IpcCommand::Resume => {
1015                        paused.store(false, Ordering::SeqCst);
1016                        for rs in render_states.values() {
1017                            let rs_lock = rs.lock().await;
1018                            rs_lock.video_playback.resume();
1019                        }
1020                        IpcResponse {
1021                            success: true,
1022                            message: Some("Resumed".into()),
1023                        }
1024                    }
1025                    IpcCommand::Reload => {
1026                        let lock = engine.lock().await;
1027                        match lock.reload() {
1028                            Ok(_) => IpcResponse {
1029                                success: true,
1030                                message: Some("Reloaded".into()),
1031                            },
1032                            Err(e) => IpcResponse {
1033                                success: false,
1034                                message: Some(e.to_string()),
1035                            },
1036                        }
1037                    }
1038                    IpcCommand::Preview {
1039                        path,
1040                        effect,
1041                        duration_ms,
1042                        no_theme,
1043                        theme_override,
1044                        monitor,
1045                        scaling_mode,
1046                    } => {
1047                        if paused.load(Ordering::SeqCst) {
1048                            return IpcResponse {
1049                                success: false,
1050                                message: Some("Daemon is paused".into()),
1051                            };
1052                        }
1053                        let p = std::path::PathBuf::from(&path);
1054                        if !p.exists() {
1055                            return IpcResponse {
1056                                success: false,
1057                                message: Some(format!("File not found: {}", path)),
1058                            };
1059                        }
1060
1061                        // Select target RenderState: explicit --monitor or first
1062                        // output.
1063                        let target_rs = if let Some(ref mon) = monitor {
1064                            render_states
1065                                .get(mon)
1066                                .cloned()
1067                                .or_else(|| render_states.values().next().cloned())
1068                        } else {
1069                            render_states.values().next().cloned()
1070                        };
1071                        let target_rs = match target_rs {
1072                            Some(rs) => rs,
1073                            None => {
1074                                return IpcResponse {
1075                                    success: false,
1076                                    message: Some("No outputs available".into()),
1077                                };
1078                            }
1079                        };
1080
1081                        // Persist per-output last wallpaper for all outputs (or
1082                        // just the targeted one when --monitor is specified).
1083                        let output_names: Vec<String> = if monitor.is_some() {
1084                            monitor
1085                                .as_ref()
1086                                .map(|m| vec![m.clone()])
1087                                .unwrap_or_default()
1088                        } else {
1089                            render_states.keys().cloned().collect()
1090                        };
1091                        for name in &output_names {
1092                            let state_path = dirs::cache_dir()
1093                                .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1094                                .join(format!("wallr/last_wallpaper/{name}"));
1095                            if let Some(parent) = state_path.parent() {
1096                                let _ = std::fs::create_dir_all(parent);
1097                            }
1098                            let _ = std::fs::write(&state_path, &path);
1099                        }
1100
1101                        let effect = effect.unwrap_or_else(|| {
1102                            crate::animation::Effect::Fade(crate::animation::FadeParams::default())
1103                        });
1104                        // Live playback only starts after the transition, so for
1105                        // videos an unrequested 2s fade reads as a long "load".
1106                        // Default to a short fade unless the user asked for one.
1107                        let is_video = crate::video::VideoDecoder::is_video_file(&p);
1108                        let duration = duration_ms.unwrap_or(if is_video { 150 } else { 2000 });
1109                        let sm = scaling_mode.unwrap_or(crate::config::ScalingMode::Fill);
1110                        let scaling_mode_u32 = match sm {
1111                            crate::config::ScalingMode::Fill => 0u32,
1112                            crate::config::ScalingMode::Fit => 1,
1113                            crate::config::ScalingMode::Stretch => 2,
1114                            crate::config::ScalingMode::Center => 3,
1115                            crate::config::ScalingMode::Tile => 4,
1116                        };
1117
1118                        let rs_clone = target_rs.clone();
1119                        let p_clone = p.clone();
1120                        let result = tokio::task::spawn_blocking(move || {
1121                            let rt = tokio::runtime::Handle::current();
1122                            rt.block_on(async {
1123                                let mut lock = rs_clone.lock().await;
1124                                lock.set_wallpaper(&p_clone, &effect, duration, scaling_mode_u32)
1125                                    .await
1126                            })
1127                        })
1128                        .await;
1129
1130                        match result {
1131                            Ok(Ok(())) => {
1132                                let opts = SetOptions {
1133                                    no_theme,
1134                                    theme_provider: theme_override,
1135                                    monitor,
1136                                };
1137                                let mut eng = engine.lock().await;
1138                                match eng.set_wallpaper(&p, &opts).await {
1139                                    Ok(()) => IpcResponse {
1140                                        success: true,
1141                                        message: None,
1142                                    },
1143                                    Err(e) => IpcResponse {
1144                                        success: true,
1145                                        message: Some(format!(
1146                                            "Wallpaper set, but hooks/theme failed: {e}"
1147                                        )),
1148                                    },
1149                                }
1150                            }
1151                            Ok(Err(e)) => IpcResponse {
1152                                success: false,
1153                                message: Some(format!("Render failed: {}", e)),
1154                            },
1155                            Err(e) => IpcResponse {
1156                                success: false,
1157                                message: Some(format!("Task spawn failed: {}", e)),
1158                            },
1159                        }
1160                    }
1161                    IpcCommand::Stop => {
1162                        let sp = stop_socket.clone();
1163                        tokio::spawn(async move {
1164                            tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1165                            let _ = std::fs::remove_file(&sp);
1166                            std::process::exit(0);
1167                        });
1168                        IpcResponse {
1169                            success: true,
1170                            message: Some("Stopping".into()),
1171                        }
1172                    }
1173                    IpcCommand::Status => {
1174                        let state = if paused.load(Ordering::SeqCst) {
1175                            "paused"
1176                        } else {
1177                            "running"
1178                        };
1179                        IpcResponse {
1180                            success: true,
1181                            message: Some(format!("wallr daemon {}", state)),
1182                        }
1183                    }
1184                    IpcCommand::Seek { timestamp_ms } => {
1185                        let target_rs = render_states.values().next().cloned();
1186                        let target_rs = match target_rs {
1187                            Some(rs) => rs,
1188                            None => {
1189                                return IpcResponse {
1190                                    success: false,
1191                                    message: Some("No outputs available".into()),
1192                                };
1193                            }
1194                        };
1195                        let rs_lock = target_rs.lock().await;
1196                        match rs_lock
1197                            .video_playback
1198                            .seek(std::time::Duration::from_millis(timestamp_ms))
1199                        {
1200                            Ok(()) => IpcResponse {
1201                                success: true,
1202                                message: Some(format!("Seeked to {}ms", timestamp_ms)),
1203                            },
1204                            Err(e) => IpcResponse {
1205                                success: false,
1206                                message: Some(format!("Seek failed: {}", e)),
1207                            },
1208                        }
1209                    }
1210                    IpcCommand::Info => {
1211                        let target_rs = render_states.values().next().cloned();
1212                        let target_rs = match target_rs {
1213                            Some(rs) => rs,
1214                            None => {
1215                                return IpcResponse {
1216                                    success: false,
1217                                    message: Some("No outputs available".into()),
1218                                };
1219                            }
1220                        };
1221                        let rs_lock = target_rs.lock().await;
1222
1223                        // Get GPU info from renderer
1224                        let gpu_info =
1225                            crate::video::gpu::adapter_diagnostics(&rs_lock.renderer.adapter);
1226
1227                        let mut lines = vec![
1228                            format!("wallr v{}", env!("CARGO_PKG_VERSION")),
1229                            String::new(),
1230                            format!("Outputs: {}", render_states.len()),
1231                        ];
1232                        for name in render_states.keys() {
1233                            lines.push(format!("  - {name}"));
1234                        }
1235                        lines.push(String::new());
1236                        lines.push(gpu_info);
1237
1238                        match rs_lock.video_playback.metadata() {
1239                            Some(meta) => {
1240                                let decoder_info = rs_lock.video_playback.decoder_info();
1241                                let hw = rs_lock.video_playback.hw_accel_in_use();
1242                                let state = if rs_lock.video_playback.is_paused() {
1243                                    "paused"
1244                                } else {
1245                                    "playing"
1246                                };
1247                                let position = rs_lock
1248                                    .video_playback
1249                                    .position()
1250                                    .map(|p| format!("{:.2}s", p.as_secs_f64()))
1251                                    .unwrap_or_else(|| "?".to_string());
1252                                lines.push(String::new());
1253                                lines.push("Video:".into());
1254                                lines.push(format!("  Resolution: {}x{}", meta.width, meta.height));
1255                                lines.push(format!("  FPS: {:.2}", meta.fps));
1256                                lines.push(format!(
1257                                    "  Duration: {:.2}s",
1258                                    meta.duration.as_secs_f64()
1259                                ));
1260                                lines.push(format!(
1261                                    "  Codec: {}",
1262                                    decoder_info
1263                                        .as_ref()
1264                                        .map(|d| d.codec_name.as_str())
1265                                        .unwrap_or("unknown")
1266                                ));
1267                                lines.push(format!("  Container: {}", meta.format));
1268                                lines.push(format!("  Decoder: {}", hw.name()));
1269                                lines.push(format!(
1270                                    "  GPU Decode: {}",
1271                                    if hw == crate::video::HwAccel::Software {
1272                                        "disabled"
1273                                    } else {
1274                                        "enabled"
1275                                    }
1276                                ));
1277                                lines.push(format!("  State: {} @ {}", state, position));
1278                            }
1279                            None => {
1280                                lines.push(String::new());
1281                                lines.push("Video: none active".into());
1282                                lines.push("Decoder: idle".into());
1283                            }
1284                        }
1285
1286                        IpcResponse {
1287                            success: true,
1288                            message: Some(lines.join("\n")),
1289                        }
1290                    }
1291                    IpcCommand::MonitorList => {
1292                        let mut lines = Vec::new();
1293                        for (name, rs) in &render_states {
1294                            let lock = rs.lock().await;
1295                            lines.push(format!("{}: {}x{}", name, lock.width, lock.height));
1296                        }
1297                        if lines.is_empty() {
1298                            IpcResponse {
1299                                success: true,
1300                                message: Some("No monitors connected".into()),
1301                            }
1302                        } else {
1303                            IpcResponse {
1304                                success: true,
1305                                message: Some(lines.join("\n")),
1306                            }
1307                        }
1308                    }
1309                    IpcCommand::MonitorCurrent => {
1310                        // Return info for the first output as "current".
1311                        if let Some((name, rs)) = render_states.iter().next() {
1312                            let lock = rs.lock().await;
1313                            IpcResponse {
1314                                success: true,
1315                                message: Some(format!("{}: {}x{}", name, lock.width, lock.height)),
1316                            }
1317                        } else {
1318                            IpcResponse {
1319                                success: false,
1320                                message: Some("No monitors connected".into()),
1321                            }
1322                        }
1323                    }
1324                }
1325            }
1326        })
1327        .await?;
1328
1329        // Start file watcher if configured
1330        if self.config.watch.enabled
1331            && let Some(ref watch_dir) = self.config.watch.dir
1332        {
1333            let watch_path = crate::config::expand_path(watch_dir);
1334            self.start_watcher(watch_path, render_states).await?;
1335        }
1336
1337        tokio::task::spawn_blocking(move || {
1338            loop {
1339                if let Err(e) = event_queue.blocking_dispatch(&mut wayland_state) {
1340                    eprintln!("Wayland dispatch error: {e:?}");
1341                    break;
1342                }
1343            }
1344            // The compositor connection is dead (e.g. the compositor exited
1345            // or killed our layer surface with a protocol error). Rendering
1346            // can never recover, so exit and let the supervisor restart us.
1347            eprintln!("wallr: Wayland connection lost, exiting");
1348            std::process::exit(1);
1349        });
1350
1351        loop {
1352            tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
1353        }
1354    }
1355
1356    async fn start_watcher(
1357        &self,
1358        dir: PathBuf,
1359        render_states: std::collections::HashMap<String, Arc<Mutex<RenderState>>>,
1360    ) -> Result<(), DaemonError> {
1361        let engine = self.engine.clone();
1362        let paused = self.paused.clone();
1363        let debounce = crate::config::parse_duration(&self.config.watch.debounce)
1364            .unwrap_or(std::time::Duration::from_millis(500));
1365
1366        let (tx, mut rx) = tokio::sync::mpsc::channel(100);
1367
1368        let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
1369            if let Ok(event) = res
1370                && let EventKind::Create(_) = event.kind
1371            {
1372                for path in event.paths {
1373                    let _ = tx.blocking_send(path);
1374                }
1375            }
1376        })
1377        .map_err(|e| DaemonError::StartError(e.to_string()))?;
1378
1379        watcher
1380            .watch(&dir, RecursiveMode::NonRecursive)
1381            .map_err(|e| DaemonError::StartError(e.to_string()))?;
1382
1383        tokio::spawn(async move {
1384            let _watcher = watcher;
1385            let mut last: Option<(PathBuf, std::time::Instant)> = None;
1386
1387            while let Some(path) = rx.recv().await {
1388                if paused.load(Ordering::SeqCst) {
1389                    continue;
1390                }
1391                if let Some((ref lp, ref lt)) = last
1392                    && lp == &path
1393                    && lt.elapsed() < debounce
1394                {
1395                    continue;
1396                }
1397                let ext = path
1398                    .extension()
1399                    .unwrap_or_default()
1400                    .to_string_lossy()
1401                    .to_lowercase();
1402                if !["jpg", "jpeg", "png", "gif", "webp"].contains(&ext.as_str()) {
1403                    continue;
1404                }
1405                last = Some((path.clone(), std::time::Instant::now()));
1406
1407                // Apply new wallpaper to every output.
1408                for (name, rs) in &render_states {
1409                    let rs = rs.clone();
1410                    let eng = engine.clone();
1411                    let p = path.clone();
1412                    let name = name.clone();
1413                    tokio::spawn(async move {
1414                        let mut lock = rs.lock().await;
1415                        let effect =
1416                            crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1417                        let _ = lock.set_wallpaper(&p, &effect, 600, 0).await;
1418                        drop(lock);
1419                        let opts = SetOptions {
1420                            no_theme: false,
1421                            theme_provider: None,
1422                            monitor: Some(name),
1423                        };
1424                        let mut elock = eng.lock().await;
1425                        let _ = elock.set_wallpaper(&p, &opts).await;
1426                    });
1427                }
1428            }
1429        });
1430
1431        Ok(())
1432    }
1433
1434    /// Creates a LayerSurface, wgpu Surface, and RenderState for a single
1435    /// Wayland output.
1436    #[allow(clippy::too_many_arguments)]
1437    async fn create_render_state_for_output(
1438        renderer: &std::sync::Arc<Renderer>,
1439        display_ptr: *mut std::ffi::c_void,
1440        wayland_state: &mut WaylandState,
1441        qh: &QueueHandle<WaylandState>,
1442        layer_shell: &LayerShell,
1443        compositor: &wl_compositor::WlCompositor,
1444        output: &OutputInfo,
1445        config: &WallrConfig,
1446    ) -> Result<RenderState, DaemonError> {
1447        let wl_surface = wayland_state.compositor_state.create_surface(qh);
1448        let layer_surface = layer_shell.create_layer_surface(
1449            qh,
1450            wl_surface,
1451            Layer::Background,
1452            Some("wallr"),
1453            Some(&output.wl_output),
1454        );
1455        layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
1456        layer_surface.set_exclusive_zone(-1);
1457        layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
1458
1459        // Empty input region so clicks pass through to the desktop.
1460        let empty_region = compositor.create_region(qh, ());
1461        layer_surface
1462            .wl_surface()
1463            .set_input_region(Some(&empty_region));
1464        layer_surface.commit();
1465        empty_region.destroy();
1466
1467        let scale_factor = if output.scale_factor > 0 {
1468            output.scale_factor
1469        } else {
1470            1
1471        };
1472        layer_surface.wl_surface().set_buffer_scale(scale_factor);
1473
1474        let width = output.width * scale_factor as u32;
1475        let height = output.height * scale_factor as u32;
1476
1477        let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
1478        wayland_state
1479            .surfaces
1480            .push((output.wl_output.id().protocol_id(), layer_surface));
1481
1482        let window_handle = WaylandWindow {
1483            display: display_ptr,
1484            surface: raw_surface,
1485        };
1486
1487        let wgpu_surface = renderer
1488            .instance
1489            .create_surface(&window_handle)
1490            .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
1491
1492        let adapter = renderer
1493            .instance
1494            .request_adapter(&wgpu::RequestAdapterOptions {
1495                compatible_surface: Some(&wgpu_surface),
1496                power_preference: wgpu::PowerPreference::HighPerformance,
1497                force_fallback_adapter: false,
1498            })
1499            .await;
1500        let surf_format = adapter
1501            .as_ref()
1502            .map(|a| {
1503                let caps = wgpu_surface.get_capabilities(a);
1504                caps.formats
1505                    .into_iter()
1506                    .next()
1507                    .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
1508            })
1509            .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb);
1510
1511        let surf_config = wgpu::SurfaceConfiguration {
1512            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1513            format: surf_format,
1514            width,
1515            height,
1516            present_mode: wgpu::PresentMode::Fifo,
1517            alpha_mode: wgpu::CompositeAlphaMode::Opaque,
1518            view_formats: vec![],
1519            desired_maximum_frame_latency: 2,
1520        };
1521        wgpu_surface.configure(&renderer.device, &surf_config);
1522
1523        // SAFETY: The surface is tied to wayland_state + window_handle, both
1524        // of which live for the entire process.
1525        let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
1526        let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
1527
1528        Ok(RenderState {
1529            renderer: renderer.clone(),
1530            surface,
1531            render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
1532            playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
1533            pacer: std::sync::Arc::new(LivePacer::new()),
1534            current_bind: None,
1535            current_tex: None,
1536            width,
1537            height,
1538            current_width: 0,
1539            current_height: 0,
1540            format: surf_format,
1541            video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
1542            hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
1543            scaling_mode: 0,
1544        })
1545    }
1546}