Skip to main content

wallr_core/daemon/
mod.rs

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