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