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