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