Skip to main content

wallr_core/daemon/
mod.rs

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