Skip to main content

wallr_core/daemon/
mod.rs

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