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, Dispatch, Proxy, QueueHandle,
30    globals::registry_queue_init,
31    protocol::{wl_compositor, wl_output, wl_surface},
32};
33use wayland_protocols::wp::viewporter::client::{
34    wp_viewport::{self, WpViewport},
35    wp_viewporter::{self, WpViewporter},
36};
37#[derive(Debug, thiserror::Error)]
38pub enum DaemonError {
39    #[error("daemon already running: {0}")]
40    AlreadyRunning(String),
41    #[error("failed to start daemon: {0}")]
42    StartError(String),
43    #[error("I/O error: {0}")]
44    Io(#[from] std::io::Error),
45    #[error("IPC error: {0}")]
46    Ipc(#[from] crate::ipc::IpcError),
47    #[error("Config error: {0}")]
48    Config(#[from] crate::config::ConfigError),
49    #[error("Wallpaper error: {0}")]
50    Wallpaper(#[from] crate::wallpaper::WallpaperError),
51}
52
53pub struct WaylandWindow {
54    pub display: *mut std::ffi::c_void,
55    pub surface: *mut std::ffi::c_void,
56}
57
58unsafe impl Send for WaylandWindow {}
59unsafe impl Sync for WaylandWindow {}
60
61impl HasWindowHandle for WaylandWindow {
62    fn window_handle(&self) -> Result<WindowHandle<'_>, raw_window_handle::HandleError> {
63        let surface = std::ptr::NonNull::new(self.surface)
64            .ok_or(raw_window_handle::HandleError::Unavailable)?;
65        let handle = WaylandWindowHandle::new(surface);
66        unsafe { Ok(WindowHandle::borrow_raw(RawWindowHandle::Wayland(handle))) }
67    }
68}
69
70impl HasDisplayHandle for WaylandWindow {
71    fn display_handle(&self) -> Result<DisplayHandle<'_>, raw_window_handle::HandleError> {
72        let display = std::ptr::NonNull::new(self.display)
73            .ok_or(raw_window_handle::HandleError::Unavailable)?;
74        let handle = WaylandDisplayHandle::new(display);
75        unsafe { Ok(DisplayHandle::borrow_raw(RawDisplayHandle::Wayland(handle))) }
76    }
77}
78
79#[derive(Clone)]
80struct OutputInfo {
81    name: String,
82    width: u32,
83    height: u32,
84    scale_factor: i32,
85    wl_output: wl_output::WlOutput,
86}
87
88#[derive(Clone)]
89struct OutputLifecycle {
90    name: String,
91    render_state: std::sync::Arc<tokio::sync::Mutex<RenderState>>,
92    active: std::sync::Arc<std::sync::atomic::AtomicBool>,
93}
94
95struct WaylandState {
96    registry_state: RegistryState,
97    output_state: OutputState,
98    compositor_state: CompositorState,
99    shm: Shm,
100    outputs: std::collections::HashMap<u32, OutputInfo>,
101    surfaces: Vec<(u32, LayerSurface)>,
102    viewporter: Option<WpViewporter>,
103    viewports: std::collections::HashMap<u32, WpViewport>,
104    output_lifecycles: std::collections::HashMap<u32, OutputLifecycle>,
105    pending_restores: std::collections::HashSet<u32>,
106    /// Layer shell protocol object for creating background surfaces.
107    layer_shell: LayerShell,
108    /// Compositor protocol object for creating input regions.
109    compositor: wl_compositor::WlCompositor,
110    /// Shared daemon context for hotplug: creates and destroys render states
111    /// when outputs appear or disappear.
112    hotplug: Option<DaemonHotplug>,
113}
114
115/// Wrapper around `*mut c_void` that implements `Send`. The pointer is a
116/// Wayland display pointer that lives for the entire process lifetime.
117struct SendDisplayPtr(*mut std::ffi::c_void);
118unsafe impl Send for SendDisplayPtr {}
119
120/// Shared context for hotplug operations. Stored in `WaylandState` so the
121/// output callbacks can create/destroy render states without needing access
122/// to the full `Daemon` state.
123struct DaemonHotplug {
124    renderer: std::sync::Arc<Renderer>,
125    config: crate::config::WallrConfig,
126    display_ptr: SendDisplayPtr,
127    /// Shared render-state map. Protected by `tokio::sync::Mutex` so the IPC
128    /// handler (async) and the Wayland callbacks (sync, via `blocking_dispatch`)
129    /// can both access it. The Wayland thread never holds this across an await,
130    /// so there is no risk of deadlocking the event loop.
131    render_states: std::sync::Arc<
132        tokio::sync::Mutex<
133            std::collections::HashMap<String, std::sync::Arc<tokio::sync::Mutex<RenderState>>>,
134        >,
135    >,
136}
137
138impl ProvidesRegistryState for WaylandState {
139    fn registry(&mut self) -> &mut RegistryState {
140        &mut self.registry_state
141    }
142
143    registry_handlers![OutputState,];
144}
145
146impl CompositorHandler for WaylandState {
147    fn scale_factor_changed(
148        &mut self,
149        _conn: &Connection,
150        _qh: &QueueHandle<Self>,
151        _surface: &wl_surface::WlSurface,
152        _new_factor: i32,
153    ) {
154    }
155    fn transform_changed(
156        &mut self,
157        _conn: &Connection,
158        _qh: &QueueHandle<Self>,
159        _surface: &wl_surface::WlSurface,
160        _new_transform: wl_output::Transform,
161    ) {
162    }
163    fn frame(
164        &mut self,
165        _conn: &Connection,
166        _qh: &QueueHandle<Self>,
167        _surface: &wl_surface::WlSurface,
168        _time: u32,
169    ) {
170    }
171    fn surface_enter(
172        &mut self,
173        _conn: &Connection,
174        _qh: &QueueHandle<Self>,
175        _surface: &wl_surface::WlSurface,
176        _output: &wl_output::WlOutput,
177    ) {
178    }
179    fn surface_leave(
180        &mut self,
181        _conn: &Connection,
182        _qh: &QueueHandle<Self>,
183        _surface: &wl_surface::WlSurface,
184        _output: &wl_output::WlOutput,
185    ) {
186    }
187}
188
189impl wayland_client::Dispatch<wayland_client::protocol::wl_region::WlRegion, ()> for WaylandState {
190    fn event(
191        _state: &mut WaylandState,
192        _region: &wayland_client::protocol::wl_region::WlRegion,
193        _event: wayland_client::protocol::wl_region::Event,
194        _data: &(),
195        _conn: &Connection,
196        _qh: &QueueHandle<WaylandState>,
197    ) {
198    }
199}
200
201impl Dispatch<WpViewporter, ()> for WaylandState {
202    fn event(
203        _state: &mut WaylandState,
204        _proxy: &WpViewporter,
205        _event: wp_viewporter::Event,
206        _data: &(),
207        _conn: &Connection,
208        _qh: &QueueHandle<WaylandState>,
209    ) {
210    }
211}
212
213impl Dispatch<WpViewport, ()> for WaylandState {
214    fn event(
215        _state: &mut WaylandState,
216        _proxy: &WpViewport,
217        _event: wp_viewport::Event,
218        _data: &(),
219        _conn: &Connection,
220        _qh: &QueueHandle<WaylandState>,
221    ) {
222    }
223}
224
225fn viewport_destination(configured: (u32, u32), physical: (u32, u32)) -> Option<(i32, i32)> {
226    let (width, height) = configured;
227    let (physical_width, physical_height) = physical;
228    match (width, height) {
229        (0, 0) => None,
230        (0, height) if physical_height > 0 => {
231            let width = (u64::from(height) * u64::from(physical_width)
232                + u64::from(physical_height) / 2)
233                / u64::from(physical_height);
234            Some((i32::try_from(width).ok()?, i32::try_from(height).ok()?))
235        }
236        (width, 0) if physical_width > 0 => {
237            let height = (u64::from(width) * u64::from(physical_height)
238                + u64::from(physical_width) / 2)
239                / u64::from(physical_width);
240            Some((i32::try_from(width).ok()?, i32::try_from(height).ok()?))
241        }
242        (width, height) => Some((i32::try_from(width).ok()?, i32::try_from(height).ok()?)),
243    }
244}
245
246#[cfg(test)]
247mod viewport_tests {
248    use super::{VideoPresentAction, video_present_action, viewport_destination};
249    use crate::renderer::FrameStatus;
250
251    #[test]
252    fn preserves_complete_configure_size() {
253        assert_eq!(
254            viewport_destination((3072, 1728), (3840, 2160)),
255            Some((3072, 1728))
256        );
257    }
258
259    #[test]
260    fn derives_missing_dimension_from_physical_aspect_ratio() {
261        assert_eq!(
262            viewport_destination((0, 1728), (3840, 2160)),
263            Some((3072, 1728))
264        );
265        assert_eq!(
266            viewport_destination((3072, 0), (3840, 2160)),
267            Some((3072, 1728))
268        );
269        assert_eq!(viewport_destination((0, 0), (3840, 2160)), None);
270    }
271
272    #[test]
273    fn retries_recoverable_video_surface_failures() {
274        assert_eq!(
275            video_present_action(FrameStatus::TimedOut),
276            VideoPresentAction::Retry
277        );
278        assert_eq!(
279            video_present_action(FrameStatus::Outdated),
280            VideoPresentAction::Reconfigure
281        );
282        assert_eq!(
283            video_present_action(FrameStatus::Lost),
284            VideoPresentAction::Reconfigure
285        );
286    }
287}
288
289impl LayerShellHandler for WaylandState {
290    fn configure(
291        &mut self,
292        _conn: &Connection,
293        _qh: &QueueHandle<Self>,
294        layer: &LayerSurface,
295        configure: LayerSurfaceConfigure,
296        _serial: u32,
297    ) {
298        // SCTK acknowledges the configure. wgpu owns subsequent buffer commits,
299        // which must not race with a bufferless commit when explicit sync is active.
300        let output_id = self.surfaces.iter().find_map(|(output_id, surface)| {
301            (surface.wl_surface().id() == layer.wl_surface().id()).then_some(*output_id)
302        });
303        let Some(output_id) = output_id else {
304            return;
305        };
306        let destination = self.outputs.get(&output_id).and_then(|output| {
307            viewport_destination(configure.new_size, (output.width, output.height))
308        });
309        if let Some(viewport) = self.viewports.get(&output_id) {
310            let Some((logical_width, logical_height)) = destination else {
311                tracing::warn!(
312                    "Output {output_id} configure omitted both dimensions; waiting for a usable size"
313                );
314                return;
315            };
316            viewport.set_destination(logical_width, logical_height);
317            tracing::debug!(
318                "Configured viewport destination for output {output_id}: {logical_width}x{logical_height}"
319            );
320        }
321        if !self.pending_restores.remove(&output_id) {
322            return;
323        }
324        let Some(lifecycle) = self.output_lifecycles.get(&output_id).cloned() else {
325            return;
326        };
327        let Some(render_states) = self
328            .hotplug
329            .as_ref()
330            .map(|hotplug| hotplug.render_states.clone())
331        else {
332            return;
333        };
334
335        tokio::spawn(async move {
336            if !lifecycle.active.load(Ordering::SeqCst) {
337                return;
338            }
339            restore_cached_wallpaper(&lifecycle.name, &lifecycle.render_state).await;
340
341            let mut states = render_states.lock().await;
342            if lifecycle.active.load(Ordering::SeqCst) {
343                states.insert(lifecycle.name.clone(), lifecycle.render_state);
344                tracing::info!("Output configured: {}", lifecycle.name);
345            }
346        });
347    }
348
349    fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {}
350}
351
352impl ShmHandler for WaylandState {
353    fn shm_state(&mut self) -> &mut Shm {
354        &mut self.shm
355    }
356}
357
358impl OutputHandler for WaylandState {
359    fn output_state(&mut self) -> &mut OutputState {
360        &mut self.output_state
361    }
362    fn new_output(
363        &mut self,
364        _conn: &Connection,
365        _qh: &QueueHandle<Self>,
366        output: wl_output::WlOutput,
367    ) {
368        let id = output.id().protocol_id();
369        tracing::info!("Output detected: protocol_id={id}");
370
371        let mut info = OutputInfo {
372            name: format!("output-{id}"),
373            width: 1920,
374            height: 1080,
375            scale_factor: 1,
376            wl_output: output,
377        };
378
379        // Resolve the compositor-provided name (e.g. "DP-1", "HDMI-A-1").
380        if let Some(info_data) = self.output_state.info(&info.wl_output) {
381            if let Some(mode) = info_data.modes.iter().find(|m| m.current) {
382                info.width = mode.dimensions.0 as u32;
383                info.height = mode.dimensions.1 as u32;
384            }
385            info.scale_factor = info_data.scale_factor;
386            let resolved = info_data
387                .name
388                .as_deref()
389                .filter(|n| !n.is_empty())
390                .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
391            if let Some(real_name) = resolved {
392                info.name = real_name.to_string();
393            } else if !info_data.make.is_empty() || !info_data.model.is_empty() {
394                let fallback = format!("{} {}", info_data.make, info_data.model)
395                    .trim()
396                    .to_string();
397                if !fallback.is_empty() {
398                    info.name = fallback;
399                }
400            }
401        }
402
403        self.outputs.insert(id, info.clone());
404
405        // Create a render state for the new output.
406        // Extract values from hotplug before passing &mut self to avoid
407        // borrow checker conflicts (hotplug is inside self).
408        if self.hotplug.is_some() {
409            let name = info.name.clone();
410            let renderer = self.hotplug.as_ref().unwrap().renderer.clone();
411            let display_ptr = self.hotplug.as_ref().unwrap().display_ptr.0;
412            let config = self.hotplug.as_ref().unwrap().config.clone();
413            match create_render_state_for_output_sync(
414                &renderer,
415                display_ptr,
416                self,
417                _qh,
418                &info,
419                &config,
420            ) {
421                Ok(rs) => {
422                    let rs = std::sync::Arc::new(tokio::sync::Mutex::new(rs));
423                    self.output_lifecycles.insert(
424                        id,
425                        OutputLifecycle {
426                            name,
427                            render_state: rs,
428                            active: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)),
429                        },
430                    );
431                    self.pending_restores.insert(id);
432                }
433                Err(e) => {
434                    tracing::error!("Hotplug: failed to create render state for {name}: {e}");
435                }
436            }
437        }
438    }
439    fn update_output(
440        &mut self,
441        _conn: &Connection,
442        _qh: &QueueHandle<Self>,
443        output: wl_output::WlOutput,
444    ) {
445        let id = output.id().protocol_id();
446        if let Some(info) = self.outputs.get_mut(&id) {
447            let mut new_width = info.width;
448            let mut new_height = info.height;
449            let mut new_scale = info.scale_factor;
450
451            if let Some(mode) = self
452                .output_state
453                .info(&output)
454                .and_then(|i| i.modes.iter().find(|m| m.current).cloned())
455            {
456                new_width = mode.dimensions.0 as u32;
457                new_height = mode.dimensions.1 as u32;
458            }
459            if let Some(info_data) = self.output_state.info(&output) {
460                new_scale = info_data.scale_factor;
461                let resolved = info_data
462                    .name
463                    .as_deref()
464                    .filter(|n| !n.is_empty())
465                    .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
466                if let Some(real_name) = resolved {
467                    if real_name != info.name {
468                        tracing::info!(
469                            "Output {id}: resolved name '{}' -> '{}'",
470                            info.name,
471                            real_name
472                        );
473                        info.name = real_name.to_string();
474                    }
475                } else if info.name.starts_with("output-")
476                    && (!info_data.make.is_empty() || !info_data.model.is_empty())
477                {
478                    let fallback = format!("{} {}", info_data.make, info_data.model)
479                        .trim()
480                        .to_string();
481                    if !fallback.is_empty() {
482                        tracing::info!(
483                            "Output {id}: fallback name '{}' -> '{}'",
484                            info.name,
485                            fallback
486                        );
487                        info.name = fallback;
488                    }
489                }
490            }
491
492            let changed = new_width != info.width
493                || new_height != info.height
494                || new_scale != info.scale_factor;
495
496            info.width = new_width;
497            info.height = new_height;
498            info.scale_factor = new_scale;
499
500            // Reconfigure the wgpu surface when dimensions or scale change.
501            if changed {
502                let name = info.name.clone();
503                if let Some(ref hotplug) = self.hotplug {
504                    let render_states = hotplug.render_states.clone();
505                    let renderer = hotplug.renderer.clone();
506                    tokio::spawn(async move {
507                        let states = render_states.lock().await;
508                        if let Some(rs) = states.get(&name) {
509                            let mut lock = rs.lock().await;
510                            lock.width = new_width;
511                            lock.height = new_height;
512                            let surf_config = wgpu::SurfaceConfiguration {
513                                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
514                                format: lock.format,
515                                width: new_width,
516                                height: new_height,
517                                present_mode: wgpu::PresentMode::Fifo,
518                                alpha_mode: wgpu::CompositeAlphaMode::Opaque,
519                                view_formats: vec![],
520                                desired_maximum_frame_latency: 2,
521                            };
522                            lock.surface.configure(&renderer.device, &surf_config);
523                            tracing::info!(
524                                "Hotplug: reconfigured {name} to {new_width}x{new_height}"
525                            );
526                        }
527                    });
528                }
529            }
530        }
531    }
532    fn output_destroyed(
533        &mut self,
534        _conn: &Connection,
535        _qh: &QueueHandle<Self>,
536        output: wl_output::WlOutput,
537    ) {
538        let id = output.id().protocol_id();
539        if let Some(info) = self.outputs.remove(&id) {
540            tracing::info!("Output disconnected: {} (protocol_id={})", info.name, id);
541            self.pending_restores.remove(&id);
542            let viewport = self.viewports.remove(&id);
543            let layer_surface = self
544                .surfaces
545                .iter()
546                .position(|(pid, _)| *pid == id)
547                .map(|position| self.surfaces.swap_remove(position).1);
548            let lifecycle = self.output_lifecycles.remove(&id);
549            if let Some(lifecycle) = &lifecycle {
550                lifecycle.active.store(false, Ordering::SeqCst);
551            }
552            // Remove the render state from the shared map and stop playback.
553            if let (Some(hotplug), Some(lifecycle)) = (&self.hotplug, lifecycle) {
554                let render_states = hotplug.render_states.clone();
555                tokio::spawn(async move {
556                    let mut states = render_states.lock().await;
557                    if states
558                        .get(&lifecycle.name)
559                        .is_some_and(|state| Arc::ptr_eq(state, &lifecycle.render_state))
560                    {
561                        states.remove(&lifecycle.name);
562                    }
563                    drop(states);
564
565                    let state = lifecycle.render_state.lock().await;
566                    state.playback_gen.fetch_add(1, Ordering::SeqCst);
567                    state.pacer.notify();
568                    state.video_playback.stop();
569                    let render_lock = state.render_lock.clone();
570                    drop(state);
571
572                    let _ = tokio::task::spawn_blocking(move || {
573                        drop(
574                            render_lock
575                                .lock()
576                                .unwrap_or_else(|poisoned| poisoned.into_inner()),
577                        );
578                    })
579                    .await;
580                    if let Some(viewport) = viewport {
581                        viewport.destroy();
582                    }
583                    drop(layer_surface);
584                    tracing::info!("Hotplug: cleaned up render state for {}", lifecycle.name);
585                });
586            } else {
587                if let Some(viewport) = viewport {
588                    viewport.destroy();
589                }
590                drop(layer_surface);
591            }
592        }
593    }
594}
595
596delegate_compositor!(WaylandState);
597delegate_layer!(WaylandState);
598delegate_output!(WaylandState);
599delegate_registry!(WaylandState);
600delegate_shm!(WaylandState);
601
602/// Wakes paced live-playback loops when a new commit bumps the generation.
603struct LivePacer {
604    lock: std::sync::Mutex<()>,
605    cond: std::sync::Condvar,
606}
607
608impl LivePacer {
609    fn new() -> Self {
610        Self {
611            lock: std::sync::Mutex::new(()),
612            cond: std::sync::Condvar::new(),
613        }
614    }
615
616    fn notify(&self) {
617        let _guard = self.lock.lock().unwrap();
618        self.cond.notify_all();
619    }
620
621    /// Blocks until `deadline` or until `notify` is called, whichever comes
622    /// first.
623    fn wait_until(&self, deadline: std::time::Instant) {
624        let guard = self.lock.lock().unwrap();
625        let now = std::time::Instant::now();
626        if deadline <= now {
627            return;
628        }
629        let _ = self
630            .cond
631            .wait_timeout_while(guard, deadline - now, |_| true);
632    }
633}
634
635struct RenderState {
636    renderer: std::sync::Arc<Renderer>,
637    surface: &'static wgpu::Surface<'static>,
638    /// Serializes transition rendering. The lock is only ever held by the
639    /// detached render task, never by the IPC loop, so a stalled present
640    /// cannot freeze the daemon.
641    render_lock: std::sync::Arc<std::sync::Mutex<()>>,
642    /// Bumped on every commit. Live playback checks it each frame and stops
643    /// as soon as a new wallpaper supersedes the one it is playing.
644    playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
645    /// Wakes paced live-playback loops when a new commit bumps the generation,
646    /// so an old player exits immediately instead of after its sleep quantum.
647    pacer: std::sync::Arc<LivePacer>,
648    current_bind: Option<wgpu::BindGroup>,
649    current_tex: Option<wgpu::Texture>,
650    width: u32,
651    height: u32,
652    current_width: u32,
653    current_height: u32,
654    format: wgpu::TextureFormat,
655    /// Video playback manager
656    video_playback: std::sync::Arc<crate::video::VideoPlayback>,
657    /// Hardware backend to request for new decoders (from `video.hw_decode`).
658    hw_accel: crate::video::HwAccel,
659    /// Maximum decoded frames buffered ahead of presentation.
660    preload_frames: usize,
661    /// Optional cap for live video presentation.
662    max_fps: Option<u32>,
663    /// Current scaling mode for live playback.
664    scaling_mode: u32,
665    /// Per-output uniform buffer + bind group (Issue #9 race fix).
666    per_output_uniforms: std::sync::Arc<crate::renderer::PerOutputUniforms>,
667    /// Path of the last wallpaper set on this output (for restore).
668    last_wallpaper: Option<std::path::PathBuf>,
669    /// Previous wallpaper state before blank (for restore).
670    pre_blank: Option<(std::path::PathBuf, u32)>,
671    /// Whether this output is currently blanked.
672    blanked: bool,
673    /// GIF playback paused state (shared with play_live task).
674    gif_paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
675}
676
677/// Everything the transition render task needs; the daemon state has already
678/// been promoted to the new wallpaper before a transition is spawned.
679struct CommitData {
680    bg_bind: wgpu::BindGroup,
681    new_bind: wgpu::BindGroup,
682    img_width: u32,
683    img_height: u32,
684    old_img_width: u32,
685    old_img_height: u32,
686    format: wgpu::TextureFormat,
687    width: u32,
688    height: u32,
689    /// Animated frames to play live after the transition, when the committed
690    /// file is a GIF.
691    animated: Option<crate::animated::AnimatedImage>,
692    /// Video metadata when committed file is a video.
693    is_video: bool,
694    /// Plane and conversion resources retained across transition and playback.
695    video_texture: Option<crate::renderer::VideoTexture>,
696    /// Playback generation captured at commit time; live playback stops when
697    /// it no longer matches `RenderState::playback_gen`.
698    generation: u64,
699    /// Scaling mode: 0=Fill, 1=Fit, 2=Stretch, 3=Center, 4=Tile.
700    scaling_mode: u32,
701    /// Optional cap for live video presentation.
702    max_fps: Option<u32>,
703}
704
705impl RenderState {
706    async fn set_wallpaper(
707        &mut self,
708        path: &std::path::Path,
709        effect: &crate::animation::Effect,
710        duration_ms: u32,
711        scaling_mode: u32,
712    ) -> anyhow::Result<()> {
713        self.scaling_mode = scaling_mode;
714        let commit = self.commit_wallpaper(path, scaling_mode)?;
715        self.spawn_transition(commit, effect, duration_ms);
716        // Update last_wallpaper after successful commit
717        self.last_wallpaper = Some(path.to_path_buf());
718        Ok(())
719    }
720
721    /// Loads the new wallpaper and atomically promotes it to the current
722    /// frame. The outgoing bind group stays alive for the transition, so the
723    /// render task can keep drawing from it after this commit returns.
724    fn commit_wallpaper(
725        &mut self,
726        path: &std::path::Path,
727        scaling_mode: u32,
728    ) -> anyhow::Result<CommitData> {
729        use image::ImageReader;
730
731        // Check if this is a video file FIRST
732        if crate::video::VideoDecoder::is_video_file(path) {
733            tracing::info!("Video file detected: {:?}", path);
734
735            // Invalidate any in-flight video render task BEFORE touching the
736            // shared decoder: the old task checks the generation on every
737            // iteration, so bumping first makes it exit (or skip the upload)
738            // before it could pull a frame of the new resolution from the
739            // freshly started decoder.
740            let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
741            self.pacer.notify();
742
743            // Start video playback (replaces any previous playback and joins
744            // its decode thread, releasing the old decoder's buffers).
745            let metadata =
746                self.video_playback
747                    .start(path, self.hw_accel, self.preload_frames, generation)?;
748
749            // Wait for the first frame so the transition's incoming image is
750            // the real first frame, not a black placeholder.
751            let first_frame = self
752                .video_playback
753                .wait_first_frame(std::time::Duration::from_millis(1000));
754
755            let (tex_width, tex_height) = first_frame
756                .as_ref()
757                .map(|frame| (frame.width, frame.height))
758                .unwrap_or((metadata.width, metadata.height));
759            let video_texture = self.renderer.create_video_texture(tex_width, tex_height);
760            let (img_width, img_height) = if let Some(frame) = first_frame {
761                self.renderer
762                    .update_video_texture(&video_texture, &frame.data)?;
763                (frame.width, frame.height)
764            } else {
765                // WebGPU initializes the output to black when no frame arrives.
766                tracing::warn!("No first frame available, using black texture");
767                (metadata.width, metadata.height)
768            };
769            let new_tex = video_texture.texture().clone();
770            let new_bind = video_texture.bind_group().clone();
771
772            let old_bind = self.current_bind.take();
773            let (old_img_width, old_img_height) = if old_bind.is_some() {
774                (self.current_width.max(1), self.current_height.max(1))
775            } else {
776                (img_width, img_height)
777            };
778            let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
779
780            drop(self.current_tex.take());
781            self.current_tex = Some(new_tex);
782            self.current_bind = Some(new_bind.clone());
783            self.current_width = img_width;
784            self.current_height = img_height;
785
786            return Ok(CommitData {
787                bg_bind,
788                new_bind,
789                img_width,
790                img_height,
791                old_img_width,
792                old_img_height,
793                format: self.format,
794                width: self.width,
795                height: self.height,
796                animated: None,
797                is_video: true,
798                video_texture: Some(video_texture),
799                generation,
800                scaling_mode,
801                max_fps: self.max_fps,
802            });
803        }
804
805        // A static image or GIF supersedes any video: release the video
806        // decoder and its buffers immediately (the generation bump also stops
807        // the video render task on its next vsync).
808        self.video_playback.stop();
809
810        // Stream animated frames (GIF) on demand during playback; the
811        // transition's incoming texture is the GIF's first frame.
812        let mut animated = crate::animated::AnimatedImage::decode(path)?;
813        let (new_tex, new_bind, img_width, img_height) = if let Some(anim) = animated.as_mut() {
814            let (w, h) = (anim.width, anim.height);
815            let (tex, bind) = self.renderer.create_texture(w, h);
816            let first = anim.first_frame();
817            if !first.is_empty() {
818                self.renderer.update_texture(&tex, first, w, h);
819            }
820            (tex, bind, w, h)
821        } else {
822            let new_img = ImageReader::open(path)?.decode()?;
823            let (tex, bind) = self.renderer.load_texture(&new_img)?;
824            (tex, bind, new_img.width(), new_img.height())
825        };
826
827        let old_bind = self.current_bind.take();
828        let (old_img_width, old_img_height) = if old_bind.is_some() {
829            (self.current_width.max(1), self.current_height.max(1))
830        } else {
831            (img_width, img_height)
832        };
833        // Keep the last image as the outgoing frame. On the first ever run,
834        // using the incoming image for both sides is a clean no-op transition;
835        // it avoids a black flash while still allowing the cached wallpaper
836        // restored at daemon startup to become the real outgoing frame.
837        let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
838
839        drop(self.current_tex.take());
840        self.current_tex = Some(new_tex);
841        self.current_bind = Some(new_bind.clone());
842        self.current_width = img_width;
843        self.current_height = img_height;
844
845        let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
846        self.pacer.notify();
847
848        Ok(CommitData {
849            bg_bind,
850            new_bind,
851            img_width,
852            img_height,
853            old_img_width,
854            old_img_height,
855            format: self.format,
856            width: self.width,
857            height: self.height,
858            animated,
859            is_video: false,
860            video_texture: None,
861            generation,
862            scaling_mode,
863            max_fps: self.max_fps,
864        })
865    }
866
867    /// Renders the committed transition on a detached blocking task. The IPC
868    /// path never waits on GPU presents, so a stalled compositor (monitor
869    /// off, suspend) cannot hang the daemon. Transitions are serialized by
870    /// the render lock: a later one simply waits until the earlier drains.
871    fn spawn_transition(
872        &self,
873        commit: CommitData,
874        effect: &crate::animation::Effect,
875        duration_ms: u32,
876    ) {
877        let renderer = self.renderer.clone();
878        let surface: &'static wgpu::Surface<'static> = self.surface;
879        let render_lock = self.render_lock.clone();
880        let playback_gen = self.playback_gen.clone();
881        let pacer = self.pacer.clone();
882        let video_playback = self.video_playback.clone();
883        let per_output_uniforms = std::sync::Arc::clone(&self.per_output_uniforms);
884        let gif_paused = self.gif_paused.clone();
885        let effect = effect.clone();
886        drop(tokio::task::spawn_blocking(move || {
887            render_transition(
888                renderer,
889                surface,
890                render_lock,
891                playback_gen,
892                pacer,
893                video_playback,
894                gif_paused,
895                commit,
896                effect,
897                duration_ms,
898                &per_output_uniforms,
899            );
900        }));
901    }
902}
903
904async fn restore_cached_wallpaper(name: &str, render_state: &Arc<Mutex<RenderState>>) {
905    let state_path = dirs::cache_dir()
906        .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
907        .join(format!("wallr/last_wallpaper/{name}"));
908    let Ok(path_str) = std::fs::read_to_string(state_path) else {
909        return;
910    };
911    let path = std::path::Path::new(path_str.trim());
912    if !path.exists() {
913        return;
914    }
915
916    let mut state = render_state.lock().await;
917    let effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
918    if let Err(err) = state.set_wallpaper(path, &effect, 1000, 0).await {
919        tracing::warn!("Failed to restore wallpaper for {name}: {err}");
920    }
921}
922
923/// Presents one frame per vsync until the wall-clock duration elapses. With
924/// PresentMode::Fifo, `get_current_texture` blocks until the previous frame
925/// is presented, so this loop is paced to the monitor refresh rate, and the
926/// transition lasts exactly `duration_ms` on any refresh rate — frame-count
927/// pacing would run too fast on high-refresh panels and too slow when the
928/// present rate is low. If the compositor stops presenting, the loop can park
929/// inside a present; that is fine here because the task is detached.
930#[allow(clippy::too_many_arguments)]
931fn render_transition(
932    renderer: std::sync::Arc<Renderer>,
933    surface: &'static wgpu::Surface<'static>,
934    render_lock: std::sync::Arc<std::sync::Mutex<()>>,
935    playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
936    pacer: std::sync::Arc<LivePacer>,
937    video_playback: std::sync::Arc<crate::video::VideoPlayback>,
938    gif_paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
939    mut commit: CommitData,
940    effect: crate::animation::Effect,
941    duration_ms: u32,
942    per_output_uniforms: &crate::renderer::PerOutputUniforms,
943) {
944    let _guard = render_lock
945        .lock()
946        .unwrap_or_else(|poisoned| poisoned.into_inner());
947
948    let duration = std::time::Duration::from_millis(u64::from(duration_ms.max(1)));
949    let start = std::time::Instant::now();
950    loop {
951        let progress = start.elapsed().as_secs_f32() / duration.as_secs_f32();
952        let uniforms = crate::animation::compute_effect_uniforms(&effect, progress.clamp(0.0, 1.0));
953        let status = renderer.render_frame(
954            crate::renderer::FrameRequest {
955                surface,
956                format: commit.format,
957                bg_bind: &commit.bg_bind,
958                new_bind: &commit.new_bind,
959                effect: &uniforms,
960                width: commit.width,
961                height: commit.height,
962                img_width: commit.img_width,
963                img_height: commit.img_height,
964                old_img_width: commit.old_img_width,
965                old_img_height: commit.old_img_height,
966                scaling_mode: commit.scaling_mode,
967            },
968            per_output_uniforms,
969        );
970        let status = match status {
971            Ok(status) => status,
972            Err(err) => {
973                eprintln!("wallr: transition render failed: {err}");
974                break;
975            }
976        };
977        if progress >= 1.0 || status != crate::renderer::FrameStatus::Presented {
978            break;
979        }
980    }
981
982    // The transition ended; if the committed wallpaper is an animated GIF and
983    // nothing superseded it while we rendered, keep the render lock and play
984    // the frames live until the next commit bumps the generation.
985    let mut animated = commit.animated.take();
986    if let Some(animated) = animated.as_mut()
987        && playback_gen.load(Ordering::SeqCst) == commit.generation
988    {
989        play_live(
990            &renderer,
991            surface,
992            &commit,
993            animated,
994            &playback_gen,
995            &pacer,
996            &gif_paused,
997            per_output_uniforms,
998        );
999    } else if commit.is_video && playback_gen.load(Ordering::SeqCst) == commit.generation {
1000        play_video(
1001            &renderer,
1002            surface,
1003            &commit,
1004            &video_playback,
1005            &playback_gen,
1006            &pacer,
1007            per_output_uniforms,
1008        );
1009    }
1010}
1011
1012/// Presents live wallpaper frames until the next commit. One frame is
1013/// presented per GIF frame boundary instead of at the monitor refresh rate.
1014/// Two textures are double-buffered and frames are decompressed directly
1015/// into a mapped staging ring (no intermediate copy), so the wake path only
1016/// presents and the pacing sleep hides the decode/upload entirely.
1017#[allow(clippy::too_many_arguments)]
1018fn play_live(
1019    renderer: &Renderer,
1020    surface: &'static wgpu::Surface<'static>,
1021    commit: &CommitData,
1022    animated: &mut crate::animated::AnimatedImage,
1023    playback_gen: &std::sync::atomic::AtomicU64,
1024    pacer: &LivePacer,
1025    gif_paused: &std::sync::Arc<std::sync::atomic::AtomicBool>,
1026    per_output_uniforms: &crate::renderer::PerOutputUniforms,
1027) {
1028    let (tex_a, bind_a) = renderer.create_texture(animated.width, animated.height);
1029    let (tex_b, bind_b) = renderer.create_texture(animated.width, animated.height);
1030    let (frame_w, frame_h) = (animated.width, animated.height);
1031    let (bytes_per_row, rows) = (frame_w * 4, frame_h);
1032    let frame_bytes = bytes_per_row as u64 * rows as u64;
1033
1034    // Map+decompress+copy path needs a byte-per-row multiple of the copy
1035    // alignment; fall back to write_texture for odd widths.
1036    let direct_upload = bytes_per_row % 256 == 0;
1037    let staging: Vec<wgpu::Buffer> = if direct_upload {
1038        (0..2)
1039            .map(|_| {
1040                renderer.device.create_buffer(&wgpu::BufferDescriptor {
1041                    label: Some("wallr-gif-staging"),
1042                    size: frame_bytes,
1043                    usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
1044                    mapped_at_creation: false,
1045                })
1046            })
1047            .collect()
1048    } else {
1049        Vec::new()
1050    };
1051
1052    let first = animated.first_frame();
1053    if !first.is_empty() {
1054        renderer.update_texture(&tex_a, first, frame_w, frame_h);
1055        renderer.update_texture(&tex_b, first, frame_w, frame_h);
1056    }
1057    let binds = [bind_a, bind_b];
1058    let textures = [tex_a, tex_b];
1059
1060    // Uploads frame `index` into `textures[tgt]`. Returns true when the GPU
1061    // copy was recorded.
1062    let upload = |renderer: &Renderer,
1063                  tgt: usize,
1064                  index: usize,
1065                  slot: usize,
1066                  animated: &mut crate::animated::AnimatedImage|
1067     -> bool {
1068        if direct_upload {
1069            let buffer = &staging[slot];
1070            let slice = buffer.slice(..);
1071            slice.map_async(wgpu::MapMode::Write, |_| {});
1072            renderer.device.poll(wgpu::Maintain::Wait);
1073            let ok = {
1074                let mut mapped = slice.get_mapped_range_mut();
1075                animated.decompress_into(index, &mut mapped)
1076            };
1077            buffer.unmap();
1078            if ok {
1079                let mut encoder = renderer
1080                    .device
1081                    .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
1082                encoder.copy_buffer_to_texture(
1083                    wgpu::TexelCopyBufferInfo {
1084                        buffer,
1085                        layout: wgpu::TexelCopyBufferLayout {
1086                            offset: 0,
1087                            bytes_per_row: Some(bytes_per_row),
1088                            rows_per_image: Some(rows),
1089                        },
1090                    },
1091                    wgpu::TexelCopyTextureInfo {
1092                        texture: &textures[tgt],
1093                        mip_level: 0,
1094                        origin: wgpu::Origin3d::ZERO,
1095                        aspect: wgpu::TextureAspect::All,
1096                    },
1097                    wgpu::Extent3d {
1098                        width: frame_w,
1099                        height: frame_h,
1100                        depth_or_array_layers: 1,
1101                    },
1102                );
1103                renderer.queue.submit([encoder.finish()]);
1104                return true;
1105            }
1106        } else {
1107            let frame = animated.frame_at(index);
1108            if !frame.is_empty() {
1109                renderer.update_texture(&textures[tgt], frame, frame_w, frame_h);
1110                return true;
1111            }
1112        }
1113        false
1114    };
1115
1116    let mut cur = 0usize; // texture index currently holding the presented frame
1117    let mut cur_frame = 0usize; // frame index currently in texture `cur`
1118    let mut next_frame = 0usize; // frame index currently in the idle texture
1119    let mut slot = 0usize; // staging ring slot for the next upload
1120    let start = std::time::Instant::now();
1121    let mut paused_elapsed = std::time::Duration::ZERO; // Accumulated pause time
1122    let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1123    loop {
1124        if playback_gen.load(Ordering::SeqCst) != commit.generation {
1125            return;
1126        }
1127
1128        // Check if paused - if so, keep presenting the current frame but don't advance
1129        if gif_paused.load(Ordering::SeqCst) {
1130            let pause_start = std::time::Instant::now();
1131            // Keep presenting the current frame while paused
1132            let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
1133            let status = renderer.render_frame(
1134                crate::renderer::FrameRequest {
1135                    surface,
1136                    format: commit.format,
1137                    bg_bind: &binds[cur],
1138                    new_bind: &binds[cur],
1139                    effect: &uniforms,
1140                    width: commit.width,
1141                    height: commit.height,
1142                    img_width: animated.width,
1143                    img_height: animated.height,
1144                    old_img_width: animated.width,
1145                    old_img_height: animated.height,
1146                    scaling_mode: commit.scaling_mode,
1147                },
1148                per_output_uniforms,
1149            );
1150            match status {
1151                Ok(crate::renderer::FrameStatus::Presented) => {}
1152                _ => return,
1153            }
1154            // Wait a bit before checking again
1155            std::thread::sleep(std::time::Duration::from_millis(16));
1156            paused_elapsed += pause_start.elapsed();
1157            continue;
1158        }
1159
1160        let index = animated.frame_index_at(start.elapsed() - paused_elapsed);
1161        if index != cur_frame {
1162            if next_frame != index {
1163                upload(renderer, cur ^ 1, index, slot, animated);
1164                slot ^= 1;
1165                next_frame = index;
1166            }
1167            cur ^= 1;
1168            cur_frame = index;
1169        }
1170        let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
1171        let status = renderer.render_frame(
1172            crate::renderer::FrameRequest {
1173                surface,
1174                format: commit.format,
1175                bg_bind: &binds[cur],
1176                new_bind: &binds[cur],
1177                effect: &uniforms,
1178                width: commit.width,
1179                height: commit.height,
1180                img_width: animated.width,
1181                img_height: animated.height,
1182                old_img_width: animated.width,
1183                old_img_height: animated.height,
1184                scaling_mode: commit.scaling_mode,
1185            },
1186            per_output_uniforms,
1187        );
1188        match status {
1189            Ok(crate::renderer::FrameStatus::Presented) => {}
1190            _ => return,
1191        }
1192
1193        // Pace to the next GIF frame boundary instead of presenting at the
1194        // monitor refresh rate: an animated wallpaper only needs a present
1195        // when its frame changes. A commit wakes us via the pacer. The
1196        // boundary is computed in absolute time (frame_start is loop-relative,
1197        // so add the completed loops) to stay correct after the animation
1198        // wraps. While waiting, warm the idle texture with the next frame so
1199        // the wake path stays on the hot critical section.
1200        let elapsed = start.elapsed() - paused_elapsed;
1201        let total: std::time::Duration = animated.total_duration();
1202        let loops = (elapsed.as_millis() / total.as_millis().max(1)) as u64;
1203        let next_change = animated.frame_start(index + 1) + total * (loops as u32);
1204        let wait = next_change.saturating_sub(elapsed);
1205        if wait > std::time::Duration::ZERO {
1206            let next = index + 1;
1207            if next_frame != next {
1208                upload(renderer, cur ^ 1, next, slot, animated);
1209                slot ^= 1;
1210                next_frame = next;
1211            }
1212            pacer.wait_until(std::time::Instant::now() + wait);
1213        }
1214    }
1215}
1216
1217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1218enum VideoPresentAction {
1219    Presented,
1220    Retry,
1221    Reconfigure,
1222}
1223
1224fn video_present_action(status: crate::renderer::FrameStatus) -> VideoPresentAction {
1225    match status {
1226        crate::renderer::FrameStatus::Presented => VideoPresentAction::Presented,
1227        crate::renderer::FrameStatus::TimedOut => VideoPresentAction::Retry,
1228        crate::renderer::FrameStatus::Outdated | crate::renderer::FrameStatus::Lost => {
1229            VideoPresentAction::Reconfigure
1230        }
1231    }
1232}
1233
1234/// Live video playback loop: continuously updates texture with decoded frames.
1235fn play_video(
1236    renderer: &Renderer,
1237    surface: &'static wgpu::Surface<'static>,
1238    commit: &CommitData,
1239    video_playback: &std::sync::Arc<crate::video::VideoPlayback>,
1240    playback_gen: &std::sync::atomic::AtomicU64,
1241    pacer: &LivePacer,
1242    per_output_uniforms: &crate::renderer::PerOutputUniforms,
1243) {
1244    let (width, height) = (commit.img_width, commit.img_height);
1245
1246    let Some(texture) = commit.video_texture.as_ref() else {
1247        tracing::warn!("Video conversion resources unavailable");
1248        return;
1249    };
1250    let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1251
1252    let min_frame_interval = commit
1253        .max_fps
1254        .filter(|fps| *fps > 0)
1255        .map(|fps| std::time::Duration::from_secs_f64(1.0 / f64::from(fps)));
1256    let mut last_present: Option<std::time::Instant> = None;
1257    let mut warned_size_mismatch = false;
1258
1259    loop {
1260        // A newer commit superseded us. Do NOT touch the shared
1261        // `video_playback` here: the successor commit already replaced the
1262        // decoder (video) or stopped it (static image), and stopping it now
1263        // would kill the successor's playback too.
1264        if playback_gen.load(Ordering::SeqCst) != commit.generation {
1265            return;
1266        }
1267
1268        if let (Some(interval), Some(previous)) = (min_frame_interval, last_present) {
1269            pacer.wait_until(previous + interval);
1270            if playback_gen.load(Ordering::SeqCst) != commit.generation {
1271                return;
1272            }
1273        }
1274
1275        // Pull the next displayable frame. The decoder queue is bounded, so
1276        // this never blocks; unchanged frames need no upload or presentation.
1277        let frame_uploaded = if let Some(frame) =
1278            video_playback.next_frame_in_generation(commit.generation)
1279        {
1280            // The shared decoder can be replaced between commits; never
1281            // upload a frame whose size does not match this task's texture.
1282            if frame.width != width || frame.height != height {
1283                if !warned_size_mismatch {
1284                    tracing::warn!(
1285                        "Skipping video frame with unexpected size {}x{} (expected {}x{})",
1286                        frame.width,
1287                        frame.height,
1288                        width,
1289                        height
1290                    );
1291                    warned_size_mismatch = true;
1292                }
1293                pacer.wait_until(std::time::Instant::now() + std::time::Duration::from_millis(2));
1294                continue;
1295            }
1296            if let Err(err) = renderer.update_video_texture(texture, &frame.data) {
1297                tracing::warn!("Video frame upload failed: {err}");
1298                return;
1299            }
1300            true
1301        } else {
1302            false
1303        };
1304
1305        if !frame_uploaded {
1306            if playback_gen.load(Ordering::SeqCst) != commit.generation {
1307                return;
1308            }
1309            let wait = video_playback
1310                .time_until_next_frame_in_generation(commit.generation)
1311                .unwrap_or(std::time::Duration::from_millis(2));
1312            if playback_gen.load(Ordering::SeqCst) != commit.generation {
1313                return;
1314            }
1315            pacer.wait_until(std::time::Instant::now() + wait);
1316            continue;
1317        }
1318
1319        let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
1320        let status = renderer.render_frame(
1321            crate::renderer::FrameRequest {
1322                surface,
1323                format: commit.format,
1324                bg_bind: texture.bind_group(),
1325                new_bind: texture.bind_group(),
1326                effect: &uniforms,
1327                width: commit.width,
1328                height: commit.height,
1329                img_width: width,
1330                img_height: height,
1331                old_img_width: width,
1332                old_img_height: height,
1333                scaling_mode: commit.scaling_mode,
1334            },
1335            per_output_uniforms,
1336        );
1337
1338        match status.map(video_present_action) {
1339            Ok(VideoPresentAction::Presented) => {
1340                last_present = Some(std::time::Instant::now());
1341            }
1342            Ok(action) => {
1343                if action == VideoPresentAction::Reconfigure {
1344                    surface.configure(
1345                        &renderer.device,
1346                        &wgpu::SurfaceConfiguration {
1347                            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1348                            format: commit.format,
1349                            width: commit.width,
1350                            height: commit.height,
1351                            present_mode: wgpu::PresentMode::Fifo,
1352                            alpha_mode: wgpu::CompositeAlphaMode::Opaque,
1353                            view_formats: vec![],
1354                            desired_maximum_frame_latency: 2,
1355                        },
1356                    );
1357                }
1358                pacer.wait_until(std::time::Instant::now() + std::time::Duration::from_millis(100));
1359            }
1360            Err(err) => {
1361                tracing::warn!("Video present failed ({err}), stopping playback");
1362                video_playback.stop();
1363                return;
1364            }
1365        }
1366    }
1367}
1368
1369/// Resolve target render states from an optional monitor name.
1370/// Returns all states when `monitor` is None, or the specific named state.
1371/// Returns an empty vec for unknown monitor names (letting callers return
1372/// an error).
1373async fn resolve_targets(
1374    render_states: &std::collections::HashMap<
1375        String,
1376        std::sync::Arc<tokio::sync::Mutex<RenderState>>,
1377    >,
1378    monitor: Option<&str>,
1379) -> Vec<std::sync::Arc<tokio::sync::Mutex<RenderState>>> {
1380    match monitor {
1381        Some(name) => {
1382            if let Some(rs) = render_states.get(name) {
1383                vec![rs.clone()]
1384            } else {
1385                Vec::new()
1386            }
1387        }
1388        None => render_states.values().cloned().collect(),
1389    }
1390}
1391
1392pub struct Daemon {
1393    config: WallrConfig,
1394    paused: Arc<AtomicBool>,
1395    engine: Arc<Mutex<WallpaperEngine>>,
1396}
1397
1398impl Daemon {
1399    pub fn new(config: WallrConfig) -> Result<Self, DaemonError> {
1400        let engine = WallpaperEngine::new(config.clone())?;
1401        Ok(Self {
1402            config,
1403            paused: Arc::new(AtomicBool::new(false)),
1404            engine: Arc::new(Mutex::new(engine)),
1405        })
1406    }
1407
1408    pub async fn start(self) -> Result<(), DaemonError> {
1409        let socket_path = crate::config::expand_path(&self.config.daemon.socket);
1410        if socket_path.exists() {
1411            if tokio::net::UnixStream::connect(&socket_path).await.is_ok() {
1412                return Err(DaemonError::AlreadyRunning(
1413                    socket_path.to_string_lossy().to_string(),
1414                ));
1415            }
1416            let _ = std::fs::remove_file(&socket_path);
1417        }
1418
1419        let renderer = Renderer::new()
1420            .await
1421            .map_err(|e| DaemonError::StartError(format!("GPU init failed: {e}")))?;
1422
1423        let conn = Connection::connect_to_env()
1424            .map_err(|e| DaemonError::StartError(format!("Failed to connect to Wayland: {e:?}")))?;
1425        let backend = conn.backend();
1426        let display_ptr = backend.display_ptr() as *mut std::ffi::c_void;
1427
1428        let (globals, mut event_queue) = registry_queue_init(&conn)
1429            .map_err(|e| DaemonError::StartError(format!("registry_queue_init failed: {e:?}")))?;
1430        let qh = event_queue.handle();
1431
1432        let compositor_state = CompositorState::bind(&globals, &qh)
1433            .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
1434        let layer_shell = LayerShell::bind(&globals, &qh)
1435            .map_err(|e| DaemonError::StartError(format!("layer_shell bind failed: {e:?}")))?;
1436        let shm = Shm::bind(&globals, &qh)
1437            .map_err(|e| DaemonError::StartError(format!("shm bind failed: {e:?}")))?;
1438
1439        // Bind compositor once for creating empty input regions (passthrough).
1440        let compositor = globals
1441            .bind::<wl_compositor::WlCompositor, WaylandState, smithay_client_toolkit::globals::GlobalData>(
1442                &qh,
1443                1..=4,
1444                smithay_client_toolkit::globals::GlobalData,
1445            )
1446            .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
1447        let viewporter = globals
1448            .bind::<WpViewporter, WaylandState, ()>(&qh, 1..=1, ())
1449            .ok();
1450        if viewporter.is_none() {
1451            tracing::warn!(
1452                "wp_viewporter is unavailable; fractional outputs will use integer buffer scaling"
1453            );
1454        }
1455
1456        let mut wayland_state = WaylandState {
1457            registry_state: RegistryState::new(&globals),
1458            output_state: OutputState::new(&globals, &qh),
1459            compositor_state,
1460            shm,
1461            outputs: std::collections::HashMap::new(),
1462            surfaces: Vec::new(),
1463            viewporter,
1464            viewports: std::collections::HashMap::new(),
1465            output_lifecycles: std::collections::HashMap::new(),
1466            pending_restores: std::collections::HashSet::new(),
1467            layer_shell,
1468            compositor,
1469            hotplug: None,
1470        };
1471
1472        // Multiple roundtrips: some compositors deliver output events lazily
1473        // across several dispatch cycles. Five roundtrips ensures all outputs
1474        // are discovered and their modes/scale are populated.
1475        for i in 0..5 {
1476            event_queue
1477                .roundtrip(&mut wayland_state)
1478                .map_err(|e| DaemonError::StartError(format!("roundtrip {i} failed: {e:?}")))?;
1479        }
1480
1481        if wayland_state.outputs.is_empty() {
1482            return Err(DaemonError::StartError(
1483                "no outputs detected after roundtrip".into(),
1484            ));
1485        }
1486
1487        tracing::info!(
1488            "Detected {} output(s): {:?}",
1489            wayland_state.outputs.len(),
1490            wayland_state
1491                .outputs
1492                .values()
1493                .map(|o| format!("{} ({}x{})", o.name, o.width, o.height))
1494                .collect::<Vec<_>>()
1495        );
1496
1497        let renderer = std::sync::Arc::new(renderer);
1498
1499        // Create a LayerSurface, wgpu Surface, and RenderState for every
1500        // known output. The key is the output's human-readable name (e.g.
1501        // "DP-1", "eDP-1") so IPC can target a specific monitor.
1502        let render_states_map: std::collections::HashMap<String, Arc<Mutex<RenderState>>> =
1503            std::collections::HashMap::new();
1504
1505        // Collect output info first so we can pass &mut wayland_state to the
1506        // helper (we need &mut to push LayerSurfaces into the surfaces vec).
1507        let output_info: Vec<(u32, OutputInfo)> = wayland_state
1508            .outputs
1509            .iter()
1510            .map(|(k, v)| {
1511                (
1512                    *k,
1513                    OutputInfo {
1514                        name: v.name.clone(),
1515                        width: v.width,
1516                        height: v.height,
1517                        scale_factor: v.scale_factor,
1518                        wl_output: v.wl_output.clone(),
1519                    },
1520                )
1521            })
1522            .collect();
1523
1524        for (proto_id, info) in &output_info {
1525            let name = info.name.clone();
1526            let rs = Self::create_render_state_for_output(
1527                &renderer,
1528                display_ptr,
1529                &mut wayland_state,
1530                &qh,
1531                info,
1532                &self.config,
1533            )
1534            .await?;
1535            let rs = Arc::new(Mutex::new(rs));
1536            wayland_state.output_lifecycles.insert(
1537                *proto_id,
1538                OutputLifecycle {
1539                    name: name.clone(),
1540                    render_state: rs,
1541                    active: Arc::new(std::sync::atomic::AtomicBool::new(true)),
1542                },
1543            );
1544            wayland_state.pending_restores.insert(*proto_id);
1545
1546            tracing::info!("Output ready: {name} ({proto_id})");
1547        }
1548
1549        // Wrap the render-state map in Arc<Mutex<...>> so it can be shared
1550        // between the Wayland event loop (hotplug) and the IPC handler.
1551        let render_states: std::sync::Arc<
1552            tokio::sync::Mutex<std::collections::HashMap<String, Arc<Mutex<RenderState>>>>,
1553        > = std::sync::Arc::new(tokio::sync::Mutex::new(render_states_map));
1554
1555        // Store the hotplug context in WaylandState so output callbacks can
1556        // create/destroy render states when outputs appear or disappear.
1557        wayland_state.hotplug = Some(DaemonHotplug {
1558            renderer: renderer.clone(),
1559            config: self.config.clone(),
1560            display_ptr: SendDisplayPtr(display_ptr),
1561            render_states: render_states.clone(),
1562        });
1563
1564        // One more roundtrip to catch outputs that appeared between the
1565        // initial roundtrips and the hotplug context being stored.
1566        event_queue
1567            .roundtrip(&mut wayland_state)
1568            .map_err(|e| DaemonError::StartError(format!("hotplug roundtrip failed: {e:?}")))?;
1569
1570        let paused_clone = self.paused.clone();
1571        let engine_clone = self.engine.clone();
1572        let render_states_clone = render_states.clone();
1573
1574        // Graceful shutdown on POSIX signals: stop video decoding, remove the
1575        // IPC socket, and exit. The compositor releases the layer-shell
1576        // surface automatically when the process exits.
1577        {
1578            let rs_map = render_states.clone();
1579            let socket_path = socket_path.clone();
1580            tokio::spawn(async move {
1581                use tokio::signal::unix::{SignalKind, signal};
1582                let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler");
1583                let mut int = signal(SignalKind::interrupt()).expect("SIGINT handler");
1584                let mut hup = signal(SignalKind::hangup()).expect("SIGHUP handler");
1585                tokio::select! {
1586                    _ = term.recv() => {}
1587                    _ = int.recv() => {}
1588                    _ = hup.recv() => {}
1589                }
1590                tracing::info!("Signal received, shutting down gracefully");
1591                let states = rs_map.lock().await;
1592                for rs in states.values() {
1593                    if let Ok(state) = rs.try_lock() {
1594                        state.video_playback.stop();
1595                    }
1596                }
1597                drop(states);
1598                let _ = std::fs::remove_file(&socket_path);
1599                std::process::exit(0);
1600            });
1601        }
1602
1603        let ipc_socket_path = socket_path.clone();
1604        start_ipc_server(&socket_path, move |cmd| {
1605            let paused = paused_clone.clone();
1606            let engine = engine_clone.clone();
1607            let render_states = render_states_clone.clone();
1608            let stop_socket = ipc_socket_path.clone();
1609            async move {
1610                let render_states = render_states.lock().await;
1611                match cmd {
1612                    IpcCommand::Pause { monitor } => {
1613                        let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1614                        if targets.is_empty() {
1615                            return IpcResponse {
1616                                success: false,
1617                                message: Some("No matching outputs".into()),
1618                            };
1619                        }
1620                        if monitor.is_none() {
1621                            paused.store(true, Ordering::SeqCst);
1622                        }
1623                        for rs in targets {
1624                            let rs_lock = rs.lock().await;
1625                            rs_lock.video_playback.pause();
1626                            rs_lock.gif_paused.store(true, Ordering::SeqCst);
1627                        }
1628                        IpcResponse {
1629                            success: true,
1630                            message: Some("Paused".into()),
1631                        }
1632                    }
1633                    IpcCommand::Resume { monitor } => {
1634                        let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1635                        if targets.is_empty() {
1636                            return IpcResponse {
1637                                success: false,
1638                                message: Some("No matching outputs".into()),
1639                            };
1640                        }
1641                        if monitor.is_none() {
1642                            paused.store(false, Ordering::SeqCst);
1643                        }
1644                        for rs in targets {
1645                            let rs_lock = rs.lock().await;
1646                            rs_lock.video_playback.resume();
1647                            rs_lock.gif_paused.store(false, Ordering::SeqCst);
1648                        }
1649                        IpcResponse {
1650                            success: true,
1651                            message: Some("Resumed".into()),
1652                        }
1653                    }
1654                    IpcCommand::Reload => {
1655                        let lock = engine.lock().await;
1656                        match lock.reload() {
1657                            Ok(_) => IpcResponse {
1658                                success: true,
1659                                message: Some("Reloaded".into()),
1660                            },
1661                            Err(e) => IpcResponse {
1662                                success: false,
1663                                message: Some(e.to_string()),
1664                            },
1665                        }
1666                    }
1667                    IpcCommand::Preview {
1668                        path,
1669                        effect,
1670                        duration_ms,
1671                        no_theme,
1672                        theme_override,
1673                        monitor,
1674                        scaling_mode,
1675                    } => {
1676                        if paused.load(Ordering::SeqCst) {
1677                            return IpcResponse {
1678                                success: false,
1679                                message: Some("Daemon is paused".into()),
1680                            };
1681                        }
1682                        let p = std::path::PathBuf::from(&path);
1683                        if !p.exists() {
1684                            return IpcResponse {
1685                                success: false,
1686                                message: Some(format!("File not found: {}", path)),
1687                            };
1688                        }
1689
1690                        // Resolve targets: unknown monitor = error, no monitor = all outputs.
1691                        let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1692                        if targets.is_empty() {
1693                            return IpcResponse {
1694                                success: false,
1695                                message: match &monitor {
1696                                    Some(name) => Some(format!("Unknown monitor: {name}")),
1697                                    None => Some("No outputs available".into()),
1698                                },
1699                            };
1700                        }
1701
1702                        // Persist per-output last wallpaper for all targeted outputs.
1703                        {
1704                            let persist_names: Vec<&str> = match &monitor {
1705                                Some(name) => vec![name.as_str()],
1706                                None => render_states.keys().map(|s| s.as_str()).collect(),
1707                            };
1708                            for name in persist_names {
1709                                let state_path = dirs::cache_dir()
1710                                    .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1711                                    .join(format!("wallr/last_wallpaper/{name}"));
1712                                if let Some(parent) = state_path.parent() {
1713                                    let _ = std::fs::create_dir_all(parent);
1714                                }
1715                                let _ = std::fs::write(&state_path, &path);
1716                            }
1717                        }
1718
1719                        let effect = effect.unwrap_or_else(|| {
1720                            crate::animation::Effect::Fade(crate::animation::FadeParams::default())
1721                        });
1722                        // Live playback only starts after the transition, so for
1723                        // videos an unrequested 2s fade reads as a long "load".
1724                        // Default to a short fade unless the user asked for one.
1725                        let is_video = crate::video::VideoDecoder::is_video_file(&p);
1726                        let duration = duration_ms.unwrap_or(if is_video { 150 } else { 2000 });
1727                        let sm = scaling_mode.unwrap_or(crate::config::ScalingMode::Fill);
1728                        let scaling_mode_u32 = match sm {
1729                            crate::config::ScalingMode::Fill => 0u32,
1730                            crate::config::ScalingMode::Fit => 1,
1731                            crate::config::ScalingMode::Stretch => 2,
1732                            crate::config::ScalingMode::Center => 3,
1733                            crate::config::ScalingMode::Tile => 4,
1734                        };
1735
1736                        let mut last_err = None;
1737                        for rs in &targets {
1738                            let rs_clone = rs.clone();
1739                            let p_clone = p.clone();
1740                            let effect_clone = effect.clone();
1741                            let result = tokio::task::spawn_blocking(move || {
1742                                let rt = tokio::runtime::Handle::current();
1743                                rt.block_on(async {
1744                                    let mut lock = rs_clone.lock().await;
1745                                    lock.set_wallpaper(
1746                                        &p_clone,
1747                                        &effect_clone,
1748                                        duration,
1749                                        scaling_mode_u32,
1750                                    )
1751                                    .await
1752                                })
1753                            })
1754                            .await;
1755
1756                            if let Err(e) = result {
1757                                last_err = Some(format!("Task spawn failed: {e}"));
1758                                continue;
1759                            }
1760                            if let Err(e) = result.unwrap() {
1761                                last_err = Some(format!("Render failed: {e}"));
1762                            }
1763                        }
1764
1765                        match last_err {
1766                            Some(e) => IpcResponse {
1767                                success: false,
1768                                message: Some(e),
1769                            },
1770                            None => {
1771                                let opts = SetOptions {
1772                                    no_theme,
1773                                    theme_provider: theme_override,
1774                                    monitor,
1775                                };
1776                                let mut eng = engine.lock().await;
1777                                match eng.set_wallpaper(&p, &opts).await {
1778                                    Ok(()) => IpcResponse {
1779                                        success: true,
1780                                        message: None,
1781                                    },
1782                                    Err(e) => IpcResponse {
1783                                        success: true,
1784                                        message: Some(format!(
1785                                            "Wallpaper set, but hooks/theme failed: {e}"
1786                                        )),
1787                                    },
1788                                }
1789                            }
1790                        }
1791                    }
1792                    IpcCommand::Stop => {
1793                        for rs in render_states.values() {
1794                            let state = rs.lock().await;
1795                            state.playback_gen.fetch_add(1, Ordering::SeqCst);
1796                            state.pacer.notify();
1797                            state.video_playback.stop();
1798                        }
1799                        let sp = stop_socket.clone();
1800                        tokio::spawn(async move {
1801                            tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1802                            let _ = std::fs::remove_file(&sp);
1803                            std::process::exit(0);
1804                        });
1805                        IpcResponse {
1806                            success: true,
1807                            message: Some("Stopping".into()),
1808                        }
1809                    }
1810                    IpcCommand::Status => {
1811                        let state = if paused.load(Ordering::SeqCst) {
1812                            "paused"
1813                        } else {
1814                            "running"
1815                        };
1816                        IpcResponse {
1817                            success: true,
1818                            message: Some(format!("wallr daemon {}", state)),
1819                        }
1820                    }
1821                    IpcCommand::Seek {
1822                        timestamp_ms,
1823                        monitor,
1824                    } => {
1825                        let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1826                        if targets.is_empty() {
1827                            return IpcResponse {
1828                                success: false,
1829                                message: match &monitor {
1830                                    Some(name) => Some(format!("Unknown monitor: {name}")),
1831                                    None => Some("No outputs available".into()),
1832                                },
1833                            };
1834                        }
1835                        // When monitor is unspecified, seek all outputs
1836                        let mut seek_count = 0u32;
1837                        let mut errors = Vec::new();
1838                        for (name, rs) in render_states.iter() {
1839                            if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
1840                                continue;
1841                            }
1842                            let rs_lock = rs.lock().await;
1843                            match rs_lock
1844                                .video_playback
1845                                .seek(std::time::Duration::from_millis(timestamp_ms))
1846                            {
1847                                Ok(()) => {
1848                                    seek_count += 1;
1849                                }
1850                                Err(e) => {
1851                                    errors.push(format!("{}: {}", name, e));
1852                                }
1853                            }
1854                        }
1855                        if seek_count == 0 {
1856                            IpcResponse {
1857                                success: false,
1858                                message: Some(format!(
1859                                    "Seek failed on all outputs: {}",
1860                                    errors.join("; ")
1861                                )),
1862                            }
1863                        } else if !errors.is_empty() {
1864                            IpcResponse {
1865                                success: true,
1866                                message: Some(format!(
1867                                    "Seeked {} output(s) to {}ms, {} failed: {}",
1868                                    seek_count,
1869                                    timestamp_ms,
1870                                    errors.len(),
1871                                    errors.join("; ")
1872                                )),
1873                            }
1874                        } else {
1875                            IpcResponse {
1876                                success: true,
1877                                message: Some(format!(
1878                                    "Seeked {} output(s) to {}ms",
1879                                    seek_count, timestamp_ms
1880                                )),
1881                            }
1882                        }
1883                    }
1884                    IpcCommand::Info { monitor } => {
1885                        let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1886                        if targets.is_empty() {
1887                            return IpcResponse {
1888                                success: false,
1889                                message: match &monitor {
1890                                    Some(name) => Some(format!("Unknown monitor: {name}")),
1891                                    None => Some("No outputs available".into()),
1892                                },
1893                            };
1894                        }
1895
1896                        let mut lines = vec![
1897                            format!("wallr v{}", env!("CARGO_PKG_VERSION")),
1898                            String::new(),
1899                            format!("Outputs: {}", render_states.len()),
1900                        ];
1901                        for name in render_states.keys() {
1902                            lines.push(format!("  - {name}"));
1903                        }
1904
1905                        // Collect target output info
1906                        for (name, rs) in render_states.iter() {
1907                            if monitor.is_some() && monitor.as_deref() != Some(name.as_str()) {
1908                                continue;
1909                            }
1910                            let rs_lock = rs.lock().await;
1911                            let gpu_info =
1912                                crate::video::gpu::adapter_diagnostics(&rs_lock.renderer.adapter);
1913
1914                            lines.push(String::new());
1915                            lines.push(format!("[{name}] {}x{}", rs_lock.width, rs_lock.height));
1916                            lines.push(gpu_info);
1917
1918                            match rs_lock.video_playback.metadata() {
1919                                Some(meta) => {
1920                                    let decoder_info = rs_lock.video_playback.decoder_info();
1921                                    let hw = rs_lock.video_playback.hw_accel_in_use();
1922                                    let state = if rs_lock.video_playback.is_paused() {
1923                                        "paused"
1924                                    } else {
1925                                        "playing"
1926                                    };
1927                                    let position = rs_lock
1928                                        .video_playback
1929                                        .position()
1930                                        .map(|p| format!("{:.2}s", p.as_secs_f64()))
1931                                        .unwrap_or_else(|| "?".to_string());
1932                                    lines.push(String::new());
1933                                    lines.push("Video:".into());
1934                                    lines.push(format!(
1935                                        "  Resolution: {}x{}",
1936                                        meta.width, meta.height
1937                                    ));
1938                                    lines.push(format!("  FPS: {:.2}", meta.fps));
1939                                    lines.push(format!(
1940                                        "  Duration: {:.2}s",
1941                                        meta.duration.as_secs_f64()
1942                                    ));
1943                                    lines.push(format!(
1944                                        "  Codec: {}",
1945                                        decoder_info
1946                                            .as_ref()
1947                                            .map(|d| d.codec_name.as_str())
1948                                            .unwrap_or("unknown")
1949                                    ));
1950                                    lines.push(format!("  Container: {}", meta.format));
1951                                    lines.push(format!("  Decoder: {}", hw.name()));
1952                                    lines.push(format!(
1953                                        "  GPU Decode: {}",
1954                                        if hw == crate::video::HwAccel::Software {
1955                                            "disabled"
1956                                        } else {
1957                                            "enabled"
1958                                        }
1959                                    ));
1960                                    lines.push(format!("  State: {} @ {}", state, position));
1961                                }
1962                                None => {
1963                                    lines.push(String::new());
1964                                    lines.push("Video: none active".into());
1965                                    lines.push("Decoder: idle".into());
1966                                }
1967                            }
1968                        }
1969
1970                        IpcResponse {
1971                            success: true,
1972                            message: Some(lines.join("\n")),
1973                        }
1974                    }
1975                    IpcCommand::MonitorList => {
1976                        let mut lines = Vec::new();
1977                        for (name, rs) in render_states.iter() {
1978                            let lock = rs.lock().await;
1979                            lines.push(format!("{}: {}x{}", name, lock.width, lock.height));
1980                        }
1981                        if lines.is_empty() {
1982                            IpcResponse {
1983                                success: true,
1984                                message: Some("No monitors connected".into()),
1985                            }
1986                        } else {
1987                            IpcResponse {
1988                                success: true,
1989                                message: Some(lines.join("\n")),
1990                            }
1991                        }
1992                    }
1993                    IpcCommand::MonitorCurrent => {
1994                        // Return info for the first output as "current".
1995                        if let Some((name, rs)) = render_states.iter().next() {
1996                            let lock = rs.lock().await;
1997                            IpcResponse {
1998                                success: true,
1999                                message: Some(format!("{}: {}x{}", name, lock.width, lock.height)),
2000                            }
2001                        } else {
2002                            IpcResponse {
2003                                success: false,
2004                                message: Some("No monitors connected".into()),
2005                            }
2006                        }
2007                    }
2008                    IpcCommand::Blank {
2009                        monitor,
2010                        effect,
2011                        duration_ms,
2012                    } => {
2013                        let targets = resolve_targets(&render_states, monitor.as_deref()).await;
2014                        if targets.is_empty() {
2015                            return IpcResponse {
2016                                success: false,
2017                                message: match &monitor {
2018                                    Some(name) => Some(format!("Unknown monitor: {name}")),
2019                                    None => Some("No outputs available".into()),
2020                                },
2021                            };
2022                        }
2023                        let mut blanked_count = 0u32;
2024                        let black_effect = effect.unwrap_or_else(|| {
2025                            crate::animation::Effect::Fade(crate::animation::FadeParams::default())
2026                        });
2027                        let duration = duration_ms.unwrap_or(800);
2028
2029                        for (name, rs) in render_states.iter() {
2030                            if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
2031                                continue;
2032                            }
2033                            let mut lock = rs.lock().await;
2034                            if lock.blanked {
2035                                continue;
2036                            }
2037                            lock.pre_blank = Some((
2038                                lock.last_wallpaper.clone().unwrap_or_default(),
2039                                lock.scaling_mode,
2040                            ));
2041                            lock.blanked = true;
2042                            let tmp = std::env::temp_dir().join("wallr_blank.png");
2043                            {
2044                                let img =
2045                                    image::RgbaImage::from_pixel(1, 1, image::Rgba([0, 0, 0, 255]));
2046                                let _ = img.save(&tmp);
2047                            }
2048                            let _ = lock.set_wallpaper(&tmp, &black_effect, duration, 0).await;
2049                            blanked_count += 1;
2050                        }
2051                        IpcResponse {
2052                            success: true,
2053                            message: Some(format!("Blanked {blanked_count} output(s)")),
2054                        }
2055                    }
2056                    IpcCommand::Restore {
2057                        monitor,
2058                        effect,
2059                        duration_ms,
2060                    } => {
2061                        let targets = resolve_targets(&render_states, monitor.as_deref()).await;
2062                        if targets.is_empty() {
2063                            return IpcResponse {
2064                                success: false,
2065                                message: match &monitor {
2066                                    Some(name) => Some(format!("Unknown monitor: {name}")),
2067                                    None => Some("No outputs available".into()),
2068                                },
2069                            };
2070                        }
2071                        let mut restored_count = 0u32;
2072                        let mut errors = Vec::new();
2073                        let restore_effect = effect.unwrap_or_else(|| {
2074                            crate::animation::Effect::Fade(crate::animation::FadeParams::default())
2075                        });
2076                        let duration = duration_ms.unwrap_or(800);
2077
2078                        for (name, rs) in render_states.iter() {
2079                            if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
2080                                continue;
2081                            }
2082                            let mut lock = rs.lock().await;
2083                            if !lock.blanked {
2084                                continue;
2085                            }
2086                            if let Some((ref path, scaling_mode)) = lock.pre_blank.clone() {
2087                                if !path.exists() {
2088                                    errors
2089                                        .push(format!("{}: wallpaper path no longer exists", name));
2090                                    lock.blanked = false;
2091                                    lock.pre_blank = None;
2092                                    continue;
2093                                }
2094                                match lock
2095                                    .set_wallpaper(
2096                                        std::path::Path::new(&path),
2097                                        &restore_effect,
2098                                        duration,
2099                                        scaling_mode,
2100                                    )
2101                                    .await
2102                                {
2103                                    Ok(_) => {
2104                                        lock.blanked = false;
2105                                        lock.pre_blank = None;
2106                                        restored_count += 1;
2107                                    }
2108                                    Err(e) => {
2109                                        errors.push(format!("{}: restore failed: {}", name, e));
2110                                        lock.blanked = false;
2111                                        lock.pre_blank = None;
2112                                    }
2113                                }
2114                            } else {
2115                                errors.push(format!("{}: no previous wallpaper to restore", name));
2116                                lock.blanked = false;
2117                            }
2118                        }
2119                        if !errors.is_empty() {
2120                            IpcResponse {
2121                                success: restored_count > 0,
2122                                message: Some(format!(
2123                                    "Restored {} output(s), {} error(s): {}",
2124                                    restored_count,
2125                                    errors.len(),
2126                                    errors.join("; ")
2127                                )),
2128                            }
2129                        } else {
2130                            IpcResponse {
2131                                success: true,
2132                                message: Some(format!("Restored {restored_count} output(s)")),
2133                            }
2134                        }
2135                    }
2136                }
2137            }
2138        })
2139        .await?;
2140
2141        // Start file watcher if configured
2142        if self.config.watch.enabled
2143            && let Some(ref watch_dir) = self.config.watch.dir
2144        {
2145            let watch_path = crate::config::expand_path(watch_dir);
2146            self.start_watcher(watch_path, render_states.clone())
2147                .await?;
2148        }
2149
2150        tokio::task::spawn_blocking(move || {
2151            loop {
2152                if let Err(e) = event_queue.blocking_dispatch(&mut wayland_state) {
2153                    eprintln!("Wayland dispatch error: {e:?}");
2154                    break;
2155                }
2156            }
2157            // The compositor connection is dead (e.g. the compositor exited
2158            // or killed our layer surface with a protocol error). Rendering
2159            // can never recover, so exit and let the supervisor restart us.
2160            eprintln!("wallr: Wayland connection lost, exiting");
2161            std::process::exit(1);
2162        });
2163
2164        loop {
2165            tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
2166        }
2167    }
2168
2169    async fn start_watcher(
2170        &self,
2171        dir: PathBuf,
2172        render_states: std::sync::Arc<
2173            tokio::sync::Mutex<std::collections::HashMap<String, Arc<Mutex<RenderState>>>>,
2174        >,
2175    ) -> Result<(), DaemonError> {
2176        let engine = self.engine.clone();
2177        let paused = self.paused.clone();
2178        let debounce = crate::config::parse_duration(&self.config.watch.debounce)
2179            .unwrap_or(std::time::Duration::from_millis(500));
2180
2181        let (tx, mut rx) = tokio::sync::mpsc::channel(100);
2182
2183        let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
2184            if let Ok(event) = res
2185                && let EventKind::Create(_) = event.kind
2186            {
2187                for path in event.paths {
2188                    let _ = tx.blocking_send(path);
2189                }
2190            }
2191        })
2192        .map_err(|e| DaemonError::StartError(e.to_string()))?;
2193
2194        watcher
2195            .watch(&dir, RecursiveMode::NonRecursive)
2196            .map_err(|e| DaemonError::StartError(e.to_string()))?;
2197
2198        tokio::spawn(async move {
2199            let _watcher = watcher;
2200            let mut last: Option<(PathBuf, std::time::Instant)> = None;
2201
2202            while let Some(path) = rx.recv().await {
2203                if paused.load(Ordering::SeqCst) {
2204                    continue;
2205                }
2206                if let Some((ref lp, ref lt)) = last
2207                    && lp == &path
2208                    && lt.elapsed() < debounce
2209                {
2210                    continue;
2211                }
2212                let ext = path
2213                    .extension()
2214                    .unwrap_or_default()
2215                    .to_string_lossy()
2216                    .to_lowercase();
2217                if !["jpg", "jpeg", "png", "gif", "webp"].contains(&ext.as_str()) {
2218                    continue;
2219                }
2220                last = Some((path.clone(), std::time::Instant::now()));
2221
2222                // Apply new wallpaper to every connected output.
2223                let states = render_states.lock().await;
2224                for (name, rs) in states.iter() {
2225                    let rs = rs.clone();
2226                    let eng = engine.clone();
2227                    let p = path.clone();
2228                    let name = name.clone();
2229                    tokio::spawn(async move {
2230                        let mut lock = rs.lock().await;
2231                        let effect =
2232                            crate::animation::Effect::Fade(crate::animation::FadeParams::default());
2233                        let _ = lock.set_wallpaper(&p, &effect, 600, 0).await;
2234                        drop(lock);
2235                        let opts = SetOptions {
2236                            no_theme: false,
2237                            theme_provider: None,
2238                            monitor: Some(name),
2239                        };
2240                        let mut elock = eng.lock().await;
2241                        let _ = elock.set_wallpaper(&p, &opts).await;
2242                    });
2243                }
2244            }
2245        });
2246
2247        Ok(())
2248    }
2249
2250    /// Creates a LayerSurface, wgpu Surface, and RenderState for a single
2251    /// Wayland output.
2252    #[allow(clippy::too_many_arguments)]
2253    async fn create_render_state_for_output(
2254        renderer: &std::sync::Arc<Renderer>,
2255        display_ptr: *mut std::ffi::c_void,
2256        wayland_state: &mut WaylandState,
2257        qh: &QueueHandle<WaylandState>,
2258        output: &OutputInfo,
2259        config: &WallrConfig,
2260    ) -> Result<RenderState, DaemonError> {
2261        let wl_surface = wayland_state.compositor_state.create_surface(qh);
2262        let layer_surface = wayland_state.layer_shell.create_layer_surface(
2263            qh,
2264            wl_surface,
2265            Layer::Background,
2266            Some("wallr"),
2267            Some(&output.wl_output),
2268        );
2269        layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
2270        layer_surface.set_exclusive_zone(-1);
2271        layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
2272
2273        // Empty input region so clicks pass through to the desktop.
2274        let empty_region = wayland_state.compositor.create_region(qh, ());
2275        layer_surface
2276            .wl_surface()
2277            .set_input_region(Some(&empty_region));
2278        let output_id = output.wl_output.id().protocol_id();
2279        let viewport = wayland_state
2280            .viewporter
2281            .as_ref()
2282            .map(|viewporter| viewporter.get_viewport(layer_surface.wl_surface(), qh, ()));
2283        let scale_factor = if viewport.is_some() {
2284            1
2285        } else if output.scale_factor > 0 {
2286            output.scale_factor
2287        } else {
2288            1
2289        };
2290        layer_surface.wl_surface().set_buffer_scale(scale_factor);
2291        layer_surface.commit();
2292        empty_region.destroy();
2293        if let Some(viewport) = viewport {
2294            wayland_state.viewports.insert(output_id, viewport);
2295        }
2296
2297        // mode.dimensions already returns physical pixels; do not multiply by scale.
2298        let width = output.width;
2299        let height = output.height;
2300
2301        let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
2302        wayland_state.surfaces.push((output_id, layer_surface));
2303
2304        let window_handle = WaylandWindow {
2305            display: display_ptr,
2306            surface: raw_surface,
2307        };
2308
2309        let wgpu_surface = renderer
2310            .instance
2311            .create_surface(&window_handle)
2312            .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
2313
2314        let adapter = renderer
2315            .instance
2316            .request_adapter(&wgpu::RequestAdapterOptions {
2317                compatible_surface: Some(&wgpu_surface),
2318                power_preference: wgpu::PowerPreference::HighPerformance,
2319                force_fallback_adapter: false,
2320            })
2321            .await;
2322        let surf_format = adapter
2323            .as_ref()
2324            .map(|a| {
2325                let caps = wgpu_surface.get_capabilities(a);
2326                caps.formats
2327                    .into_iter()
2328                    .next()
2329                    .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
2330            })
2331            .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb);
2332
2333        let surf_config = wgpu::SurfaceConfiguration {
2334            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2335            format: surf_format,
2336            width,
2337            height,
2338            present_mode: wgpu::PresentMode::Fifo,
2339            alpha_mode: wgpu::CompositeAlphaMode::Opaque,
2340            view_formats: vec![],
2341            desired_maximum_frame_latency: 2,
2342        };
2343        wgpu_surface.configure(&renderer.device, &surf_config);
2344
2345        // SAFETY: The surface is tied to wayland_state + window_handle, both
2346        // of which live for the entire process.
2347        let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
2348        let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
2349
2350        Ok(RenderState {
2351            renderer: renderer.clone(),
2352            surface,
2353            render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
2354            playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
2355            pacer: std::sync::Arc::new(LivePacer::new()),
2356            current_bind: None,
2357            current_tex: None,
2358            width,
2359            height,
2360            current_width: 0,
2361            current_height: 0,
2362            format: surf_format,
2363            video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
2364            hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
2365            preload_frames: config.video.preload_frames,
2366            max_fps: config.daemon.max_fps,
2367            scaling_mode: 0,
2368            per_output_uniforms: std::sync::Arc::new(renderer.create_per_output_uniforms()),
2369            last_wallpaper: None,
2370            pre_blank: None,
2371            blanked: false,
2372            gif_paused: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2373        })
2374    }
2375}
2376
2377/// Synchronous version of `Daemon::create_render_state_for_output` for hotplug.
2378/// Reuses the existing adapter from the renderer instead of requesting a
2379/// new one, avoiding the async requirement.
2380#[allow(clippy::too_many_arguments)]
2381fn create_render_state_for_output_sync(
2382    renderer: &std::sync::Arc<Renderer>,
2383    display_ptr: *mut std::ffi::c_void,
2384    wayland_state: &mut WaylandState,
2385    qh: &QueueHandle<WaylandState>,
2386    output: &OutputInfo,
2387    config: &WallrConfig,
2388) -> Result<RenderState, DaemonError> {
2389    let wl_surface = wayland_state.compositor_state.create_surface(qh);
2390    let layer_surface = wayland_state.layer_shell.create_layer_surface(
2391        qh,
2392        wl_surface,
2393        Layer::Background,
2394        Some("wallr"),
2395        Some(&output.wl_output),
2396    );
2397    layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
2398    layer_surface.set_exclusive_zone(-1);
2399    layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
2400
2401    let empty_region = wayland_state.compositor.create_region(qh, ());
2402    layer_surface
2403        .wl_surface()
2404        .set_input_region(Some(&empty_region));
2405    let output_id = output.wl_output.id().protocol_id();
2406    let viewport = wayland_state
2407        .viewporter
2408        .as_ref()
2409        .map(|viewporter| viewporter.get_viewport(layer_surface.wl_surface(), qh, ()));
2410    let scale_factor = if viewport.is_some() {
2411        1
2412    } else if output.scale_factor > 0 {
2413        output.scale_factor
2414    } else {
2415        1
2416    };
2417    layer_surface.wl_surface().set_buffer_scale(scale_factor);
2418    layer_surface.commit();
2419    empty_region.destroy();
2420    if let Some(viewport) = viewport {
2421        wayland_state.viewports.insert(output_id, viewport);
2422    }
2423
2424    // mode.dimensions already returns physical pixels; do not multiply by scale.
2425    let width = output.width;
2426    let height = output.height;
2427
2428    let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
2429    wayland_state.surfaces.push((output_id, layer_surface));
2430
2431    let window_handle = WaylandWindow {
2432        display: display_ptr,
2433        surface: raw_surface,
2434    };
2435
2436    let wgpu_surface = renderer
2437        .instance
2438        .create_surface(&window_handle)
2439        .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
2440
2441    let surf_format = {
2442        let caps = wgpu_surface.get_capabilities(&renderer.adapter);
2443        caps.formats
2444            .into_iter()
2445            .next()
2446            .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
2447    };
2448
2449    let surf_config = wgpu::SurfaceConfiguration {
2450        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2451        format: surf_format,
2452        width,
2453        height,
2454        present_mode: wgpu::PresentMode::Fifo,
2455        alpha_mode: wgpu::CompositeAlphaMode::Opaque,
2456        view_formats: vec![],
2457        desired_maximum_frame_latency: 2,
2458    };
2459    wgpu_surface.configure(&renderer.device, &surf_config);
2460
2461    let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
2462    let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
2463
2464    Ok(RenderState {
2465        renderer: renderer.clone(),
2466        surface,
2467        render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
2468        playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
2469        pacer: std::sync::Arc::new(LivePacer::new()),
2470        current_bind: None,
2471        current_tex: None,
2472        width,
2473        height,
2474        current_width: 0,
2475        current_height: 0,
2476        format: surf_format,
2477        video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
2478        hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
2479        preload_frames: config.video.preload_frames,
2480        max_fps: config.daemon.max_fps,
2481        scaling_mode: 0,
2482        per_output_uniforms: std::sync::Arc::new(renderer.create_per_output_uniforms()),
2483        last_wallpaper: None,
2484        pre_blank: None,
2485        blanked: false,
2486        gif_paused: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2487    })
2488}