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