1use crate::config::WallrConfig;
2use crate::ipc::{IpcCommand, IpcResponse, start_ipc_server};
3use crate::renderer::Renderer;
4use crate::wallpaper::{SetOptions, WallpaperEngine};
5use notify::{Event, EventKind, RecursiveMode, Watcher};
6use std::path::PathBuf;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, Ordering};
9use tokio::sync::Mutex;
10
11use raw_window_handle::{
12 DisplayHandle, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle,
13 WaylandDisplayHandle, WaylandWindowHandle, WindowHandle,
14};
15use smithay_client_toolkit::{
16 compositor::{CompositorHandler, CompositorState},
17 delegate_compositor, delegate_layer, delegate_output, delegate_registry, delegate_shm,
18 output::{OutputHandler, OutputState},
19 registry::{ProvidesRegistryState, RegistryState},
20 registry_handlers,
21 shell::WaylandSurface,
22 shell::wlr_layer::{
23 Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
24 LayerSurfaceConfigure,
25 },
26 shm::{Shm, ShmHandler},
27};
28use wayland_client::{
29 Connection, Dispatch, Proxy, QueueHandle,
30 globals::registry_queue_init,
31 protocol::{wl_compositor, wl_output, wl_surface},
32};
33use wayland_protocols::wp::viewporter::client::{
34 wp_viewport::{self, WpViewport},
35 wp_viewporter::{self, WpViewporter},
36};
37#[derive(Debug, thiserror::Error)]
38pub enum DaemonError {
39 #[error("daemon already running: {0}")]
40 AlreadyRunning(String),
41 #[error("failed to start daemon: {0}")]
42 StartError(String),
43 #[error("I/O error: {0}")]
44 Io(#[from] std::io::Error),
45 #[error("IPC error: {0}")]
46 Ipc(#[from] crate::ipc::IpcError),
47 #[error("Config error: {0}")]
48 Config(#[from] crate::config::ConfigError),
49 #[error("Wallpaper error: {0}")]
50 Wallpaper(#[from] crate::wallpaper::WallpaperError),
51}
52
53pub struct WaylandWindow {
54 pub display: *mut std::ffi::c_void,
55 pub surface: *mut std::ffi::c_void,
56}
57
58unsafe impl Send for WaylandWindow {}
59unsafe impl Sync for WaylandWindow {}
60
61impl HasWindowHandle for WaylandWindow {
62 fn window_handle(&self) -> Result<WindowHandle<'_>, raw_window_handle::HandleError> {
63 let surface = std::ptr::NonNull::new(self.surface)
64 .ok_or(raw_window_handle::HandleError::Unavailable)?;
65 let handle = WaylandWindowHandle::new(surface);
66 unsafe { Ok(WindowHandle::borrow_raw(RawWindowHandle::Wayland(handle))) }
67 }
68}
69
70impl HasDisplayHandle for WaylandWindow {
71 fn display_handle(&self) -> Result<DisplayHandle<'_>, raw_window_handle::HandleError> {
72 let display = std::ptr::NonNull::new(self.display)
73 .ok_or(raw_window_handle::HandleError::Unavailable)?;
74 let handle = WaylandDisplayHandle::new(display);
75 unsafe { Ok(DisplayHandle::borrow_raw(RawDisplayHandle::Wayland(handle))) }
76 }
77}
78
79#[derive(Clone)]
80struct OutputInfo {
81 name: String,
82 width: u32,
83 height: u32,
84 scale_factor: i32,
85 wl_output: wl_output::WlOutput,
86}
87
88#[derive(Clone)]
89struct OutputLifecycle {
90 name: String,
91 render_state: std::sync::Arc<tokio::sync::Mutex<RenderState>>,
92 active: std::sync::Arc<std::sync::atomic::AtomicBool>,
93}
94
95struct WaylandState {
96 registry_state: RegistryState,
97 output_state: OutputState,
98 compositor_state: CompositorState,
99 shm: Shm,
100 outputs: std::collections::HashMap<u32, OutputInfo>,
101 surfaces: Vec<(u32, LayerSurface)>,
102 viewporter: Option<WpViewporter>,
103 viewports: std::collections::HashMap<u32, WpViewport>,
104 output_lifecycles: std::collections::HashMap<u32, OutputLifecycle>,
105 pending_restores: std::collections::HashSet<u32>,
106 layer_shell: LayerShell,
108 compositor: wl_compositor::WlCompositor,
110 hotplug: Option<DaemonHotplug>,
113}
114
115struct SendDisplayPtr(*mut std::ffi::c_void);
118unsafe impl Send for SendDisplayPtr {}
119
120struct DaemonHotplug {
124 renderer: std::sync::Arc<Renderer>,
125 config: crate::config::WallrConfig,
126 display_ptr: SendDisplayPtr,
127 render_states: std::sync::Arc<
132 tokio::sync::Mutex<
133 std::collections::HashMap<String, std::sync::Arc<tokio::sync::Mutex<RenderState>>>,
134 >,
135 >,
136}
137
138impl ProvidesRegistryState for WaylandState {
139 fn registry(&mut self) -> &mut RegistryState {
140 &mut self.registry_state
141 }
142
143 registry_handlers![OutputState,];
144}
145
146impl CompositorHandler for WaylandState {
147 fn scale_factor_changed(
148 &mut self,
149 _conn: &Connection,
150 _qh: &QueueHandle<Self>,
151 _surface: &wl_surface::WlSurface,
152 _new_factor: i32,
153 ) {
154 }
155 fn transform_changed(
156 &mut self,
157 _conn: &Connection,
158 _qh: &QueueHandle<Self>,
159 _surface: &wl_surface::WlSurface,
160 _new_transform: wl_output::Transform,
161 ) {
162 }
163 fn frame(
164 &mut self,
165 _conn: &Connection,
166 _qh: &QueueHandle<Self>,
167 _surface: &wl_surface::WlSurface,
168 _time: u32,
169 ) {
170 }
171 fn surface_enter(
172 &mut self,
173 _conn: &Connection,
174 _qh: &QueueHandle<Self>,
175 _surface: &wl_surface::WlSurface,
176 _output: &wl_output::WlOutput,
177 ) {
178 }
179 fn surface_leave(
180 &mut self,
181 _conn: &Connection,
182 _qh: &QueueHandle<Self>,
183 _surface: &wl_surface::WlSurface,
184 _output: &wl_output::WlOutput,
185 ) {
186 }
187}
188
189impl wayland_client::Dispatch<wayland_client::protocol::wl_region::WlRegion, ()> for WaylandState {
190 fn event(
191 _state: &mut WaylandState,
192 _region: &wayland_client::protocol::wl_region::WlRegion,
193 _event: wayland_client::protocol::wl_region::Event,
194 _data: &(),
195 _conn: &Connection,
196 _qh: &QueueHandle<WaylandState>,
197 ) {
198 }
199}
200
201impl Dispatch<WpViewporter, ()> for WaylandState {
202 fn event(
203 _state: &mut WaylandState,
204 _proxy: &WpViewporter,
205 _event: wp_viewporter::Event,
206 _data: &(),
207 _conn: &Connection,
208 _qh: &QueueHandle<WaylandState>,
209 ) {
210 }
211}
212
213impl Dispatch<WpViewport, ()> for WaylandState {
214 fn event(
215 _state: &mut WaylandState,
216 _proxy: &WpViewport,
217 _event: wp_viewport::Event,
218 _data: &(),
219 _conn: &Connection,
220 _qh: &QueueHandle<WaylandState>,
221 ) {
222 }
223}
224
225fn viewport_destination(configured: (u32, u32), physical: (u32, u32)) -> Option<(i32, i32)> {
226 let (width, height) = configured;
227 let (physical_width, physical_height) = physical;
228 match (width, height) {
229 (0, 0) => None,
230 (0, height) if physical_height > 0 => {
231 let width = (u64::from(height) * u64::from(physical_width)
232 + u64::from(physical_height) / 2)
233 / u64::from(physical_height);
234 Some((i32::try_from(width).ok()?, i32::try_from(height).ok()?))
235 }
236 (width, 0) if physical_width > 0 => {
237 let height = (u64::from(width) * u64::from(physical_height)
238 + u64::from(physical_width) / 2)
239 / u64::from(physical_width);
240 Some((i32::try_from(width).ok()?, i32::try_from(height).ok()?))
241 }
242 (width, height) => Some((i32::try_from(width).ok()?, i32::try_from(height).ok()?)),
243 }
244}
245
246#[cfg(test)]
247mod viewport_tests {
248 use super::{
249 VideoPresentAction, is_transient_wallpaper_error, persist_wallpaper_at,
250 read_wallpaper_state, video_present_action, viewport_destination, write_wallpaper_state,
251 };
252 use crate::renderer::FrameStatus;
253
254 #[test]
255 fn preserves_complete_configure_size() {
256 assert_eq!(
257 viewport_destination((3072, 1728), (3840, 2160)),
258 Some((3072, 1728))
259 );
260 }
261
262 #[test]
263 fn derives_missing_dimension_from_physical_aspect_ratio() {
264 assert_eq!(
265 viewport_destination((0, 1728), (3840, 2160)),
266 Some((3072, 1728))
267 );
268 assert_eq!(
269 viewport_destination((3072, 0), (3840, 2160)),
270 Some((3072, 1728))
271 );
272 assert_eq!(viewport_destination((0, 0), (3840, 2160)), None);
273 }
274
275 #[test]
276 fn retries_recoverable_video_surface_failures() {
277 assert_eq!(
278 video_present_action(FrameStatus::TimedOut),
279 VideoPresentAction::Retry
280 );
281 assert_eq!(
282 video_present_action(FrameStatus::Outdated),
283 VideoPresentAction::Reconfigure
284 );
285 assert_eq!(
286 video_present_action(FrameStatus::Lost),
287 VideoPresentAction::Reconfigure
288 );
289 }
290
291 #[test]
292 fn rotates_wallpaper_state_only_after_successful_persistence() {
293 let temporary = tempfile::tempdir().expect("temporary directory");
294 let first = temporary.path().join("first.jpg");
295 let second = temporary.path().join("second.jpg");
296 std::fs::write(&first, b"first").expect("first wallpaper");
297 std::fs::write(&second, b"second").expect("second wallpaper");
298
299 persist_wallpaper_at(temporary.path(), "DP-1", &first).expect("persist first");
300 assert_eq!(
301 read_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1"),
302 Some(first.clone())
303 );
304 assert_eq!(
305 read_wallpaper_state(temporary.path(), "previous_wallpaper", "DP-1"),
306 None
307 );
308
309 persist_wallpaper_at(temporary.path(), "DP-1", &second).expect("persist second");
310 assert_eq!(
311 read_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1"),
312 Some(second)
313 );
314 assert_eq!(
315 read_wallpaper_state(temporary.path(), "previous_wallpaper", "DP-1"),
316 Some(first)
317 );
318
319 let missing = temporary.path().join("missing.jpg");
320 write_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1", &missing)
321 .expect("persist missing path for restore test");
322 assert_eq!(
323 read_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1"),
324 Some(missing)
325 );
326 }
327
328 #[test]
329 fn wallpaper_state_preserves_non_utf8_and_whitespace() {
330 use std::os::unix::ffi::OsStringExt;
331
332 let temporary = tempfile::tempdir().expect("temporary directory");
333 let wallpaper = std::path::PathBuf::from(std::ffi::OsString::from_vec(
334 b" /tmp/wallpaper-\xff.jpg ".to_vec(),
335 ));
336
337 write_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1", &wallpaper)
338 .expect("persist wallpaper path");
339
340 assert_eq!(
341 read_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1"),
342 Some(wallpaper)
343 );
344 }
345
346 #[test]
347 fn retries_only_explicitly_transient_errors() {
348 let timed_out = anyhow::Error::new(std::io::Error::new(
349 std::io::ErrorKind::TimedOut,
350 "temporary timeout",
351 ));
352 let missing = anyhow::Error::new(std::io::Error::new(
353 std::io::ErrorKind::NotFound,
354 "missing wallpaper",
355 ));
356 let recoverable_video = anyhow::Error::new(crate::video::VideoError::QueueFull);
357
358 assert!(is_transient_wallpaper_error(&timed_out));
359 assert!(is_transient_wallpaper_error(&recoverable_video));
360 assert!(!is_transient_wallpaper_error(&missing));
361 assert!(!is_transient_wallpaper_error(&anyhow::anyhow!(
362 "invalid dimensions"
363 )));
364 }
365}
366
367impl LayerShellHandler for WaylandState {
368 fn configure(
369 &mut self,
370 _conn: &Connection,
371 _qh: &QueueHandle<Self>,
372 layer: &LayerSurface,
373 configure: LayerSurfaceConfigure,
374 _serial: u32,
375 ) {
376 let output_id = self.surfaces.iter().find_map(|(output_id, surface)| {
379 (surface.wl_surface().id() == layer.wl_surface().id()).then_some(*output_id)
380 });
381 let Some(output_id) = output_id else {
382 return;
383 };
384 let destination = self.outputs.get(&output_id).and_then(|output| {
385 viewport_destination(configure.new_size, (output.width, output.height))
386 });
387 if let Some(viewport) = self.viewports.get(&output_id) {
388 let Some((logical_width, logical_height)) = destination else {
389 tracing::warn!(
390 "Output {output_id} configure omitted both dimensions; waiting for a usable size"
391 );
392 return;
393 };
394 viewport.set_destination(logical_width, logical_height);
395 tracing::debug!(
396 "Configured viewport destination for output {output_id}: {logical_width}x{logical_height}"
397 );
398 }
399 if !self.pending_restores.remove(&output_id) {
400 return;
401 }
402 let Some(lifecycle) = self.output_lifecycles.get(&output_id).cloned() else {
403 return;
404 };
405 let Some(render_states) = self
406 .hotplug
407 .as_ref()
408 .map(|hotplug| hotplug.render_states.clone())
409 else {
410 return;
411 };
412
413 tokio::spawn(async move {
414 if !lifecycle.active.load(Ordering::SeqCst) {
415 return;
416 }
417 restore_cached_wallpaper(&lifecycle.name, &lifecycle.render_state).await;
418
419 let mut states = render_states.lock().await;
420 if lifecycle.active.load(Ordering::SeqCst) {
421 states.insert(lifecycle.name.clone(), lifecycle.render_state);
422 tracing::info!("Output configured: {}", lifecycle.name);
423 }
424 });
425 }
426
427 fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {}
428}
429
430impl ShmHandler for WaylandState {
431 fn shm_state(&mut self) -> &mut Shm {
432 &mut self.shm
433 }
434}
435
436impl OutputHandler for WaylandState {
437 fn output_state(&mut self) -> &mut OutputState {
438 &mut self.output_state
439 }
440 fn new_output(
441 &mut self,
442 _conn: &Connection,
443 _qh: &QueueHandle<Self>,
444 output: wl_output::WlOutput,
445 ) {
446 let id = output.id().protocol_id();
447 tracing::info!("Output detected: protocol_id={id}");
448
449 let mut info = OutputInfo {
450 name: format!("output-{id}"),
451 width: 1920,
452 height: 1080,
453 scale_factor: 1,
454 wl_output: output,
455 };
456
457 if let Some(info_data) = self.output_state.info(&info.wl_output) {
459 if let Some(mode) = info_data.modes.iter().find(|m| m.current) {
460 info.width = mode.dimensions.0 as u32;
461 info.height = mode.dimensions.1 as u32;
462 }
463 info.scale_factor = info_data.scale_factor;
464 let resolved = info_data
465 .name
466 .as_deref()
467 .filter(|n| !n.is_empty())
468 .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
469 if let Some(real_name) = resolved {
470 info.name = real_name.to_string();
471 } else if !info_data.make.is_empty() || !info_data.model.is_empty() {
472 let fallback = format!("{} {}", info_data.make, info_data.model)
473 .trim()
474 .to_string();
475 if !fallback.is_empty() {
476 info.name = fallback;
477 }
478 }
479 }
480
481 self.outputs.insert(id, info.clone());
482
483 if self.hotplug.is_some() {
487 let name = info.name.clone();
488 let renderer = self.hotplug.as_ref().unwrap().renderer.clone();
489 let display_ptr = self.hotplug.as_ref().unwrap().display_ptr.0;
490 let config = self.hotplug.as_ref().unwrap().config.clone();
491 match create_render_state_for_output_sync(
492 &renderer,
493 display_ptr,
494 self,
495 _qh,
496 &info,
497 &config,
498 ) {
499 Ok(rs) => {
500 let rs = std::sync::Arc::new(tokio::sync::Mutex::new(rs));
501 self.output_lifecycles.insert(
502 id,
503 OutputLifecycle {
504 name,
505 render_state: rs,
506 active: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)),
507 },
508 );
509 self.pending_restores.insert(id);
510 }
511 Err(e) => {
512 tracing::error!("Hotplug: failed to create render state for {name}: {e}");
513 }
514 }
515 }
516 }
517 fn update_output(
518 &mut self,
519 _conn: &Connection,
520 _qh: &QueueHandle<Self>,
521 output: wl_output::WlOutput,
522 ) {
523 let id = output.id().protocol_id();
524 if let Some(info) = self.outputs.get_mut(&id) {
525 let mut new_width = info.width;
526 let mut new_height = info.height;
527 let mut new_scale = info.scale_factor;
528
529 if let Some(mode) = self
530 .output_state
531 .info(&output)
532 .and_then(|i| i.modes.iter().find(|m| m.current).cloned())
533 {
534 new_width = mode.dimensions.0 as u32;
535 new_height = mode.dimensions.1 as u32;
536 }
537 if let Some(info_data) = self.output_state.info(&output) {
538 new_scale = info_data.scale_factor;
539 let resolved = info_data
540 .name
541 .as_deref()
542 .filter(|n| !n.is_empty())
543 .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
544 if let Some(real_name) = resolved {
545 if real_name != info.name {
546 tracing::info!(
547 "Output {id}: resolved name '{}' -> '{}'",
548 info.name,
549 real_name
550 );
551 info.name = real_name.to_string();
552 }
553 } else if info.name.starts_with("output-")
554 && (!info_data.make.is_empty() || !info_data.model.is_empty())
555 {
556 let fallback = format!("{} {}", info_data.make, info_data.model)
557 .trim()
558 .to_string();
559 if !fallback.is_empty() {
560 tracing::info!(
561 "Output {id}: fallback name '{}' -> '{}'",
562 info.name,
563 fallback
564 );
565 info.name = fallback;
566 }
567 }
568 }
569
570 let changed = new_width != info.width
571 || new_height != info.height
572 || new_scale != info.scale_factor;
573
574 info.width = new_width;
575 info.height = new_height;
576 info.scale_factor = new_scale;
577
578 if changed {
580 let name = info.name.clone();
581 if let Some(ref hotplug) = self.hotplug {
582 let render_states = hotplug.render_states.clone();
583 let renderer = hotplug.renderer.clone();
584 tokio::spawn(async move {
585 let states = render_states.lock().await;
586 if let Some(rs) = states.get(&name) {
587 let mut lock = rs.lock().await;
588 lock.width = new_width;
589 lock.height = new_height;
590 let surf_config = wgpu::SurfaceConfiguration {
591 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
592 format: lock.format,
593 width: new_width,
594 height: new_height,
595 present_mode: wgpu::PresentMode::Fifo,
596 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
597 view_formats: vec![],
598 desired_maximum_frame_latency: 2,
599 };
600 lock.surface.configure(&renderer.device, &surf_config);
601 tracing::info!(
602 "Hotplug: reconfigured {name} to {new_width}x{new_height}"
603 );
604 }
605 });
606 }
607 }
608 }
609 }
610 fn output_destroyed(
611 &mut self,
612 _conn: &Connection,
613 _qh: &QueueHandle<Self>,
614 output: wl_output::WlOutput,
615 ) {
616 let id = output.id().protocol_id();
617 if let Some(info) = self.outputs.remove(&id) {
618 tracing::info!("Output disconnected: {} (protocol_id={})", info.name, id);
619 self.pending_restores.remove(&id);
620 let viewport = self.viewports.remove(&id);
621 let layer_surface = self
622 .surfaces
623 .iter()
624 .position(|(pid, _)| *pid == id)
625 .map(|position| self.surfaces.swap_remove(position).1);
626 let lifecycle = self.output_lifecycles.remove(&id);
627 if let Some(lifecycle) = &lifecycle {
628 lifecycle.active.store(false, Ordering::SeqCst);
629 }
630 if let (Some(hotplug), Some(lifecycle)) = (&self.hotplug, lifecycle) {
632 let render_states = hotplug.render_states.clone();
633 tokio::spawn(async move {
634 let mut states = render_states.lock().await;
635 if states
636 .get(&lifecycle.name)
637 .is_some_and(|state| Arc::ptr_eq(state, &lifecycle.render_state))
638 {
639 states.remove(&lifecycle.name);
640 }
641 drop(states);
642
643 let state = lifecycle.render_state.lock().await;
644 state.playback_gen.fetch_add(1, Ordering::SeqCst);
645 state.pacer.notify();
646 state.video_playback.stop();
647 let render_lock = state.render_lock.clone();
648 drop(state);
649
650 let _ = tokio::task::spawn_blocking(move || {
651 drop(
652 render_lock
653 .lock()
654 .unwrap_or_else(|poisoned| poisoned.into_inner()),
655 );
656 })
657 .await;
658 if let Some(viewport) = viewport {
659 viewport.destroy();
660 }
661 drop(layer_surface);
662 tracing::info!("Hotplug: cleaned up render state for {}", lifecycle.name);
663 });
664 } else {
665 if let Some(viewport) = viewport {
666 viewport.destroy();
667 }
668 drop(layer_surface);
669 }
670 }
671 }
672}
673
674delegate_compositor!(WaylandState);
675delegate_layer!(WaylandState);
676delegate_output!(WaylandState);
677delegate_registry!(WaylandState);
678delegate_shm!(WaylandState);
679
680struct LivePacer {
682 lock: std::sync::Mutex<()>,
683 cond: std::sync::Condvar,
684}
685
686impl LivePacer {
687 fn new() -> Self {
688 Self {
689 lock: std::sync::Mutex::new(()),
690 cond: std::sync::Condvar::new(),
691 }
692 }
693
694 fn notify(&self) {
695 let _guard = self.lock.lock().unwrap();
696 self.cond.notify_all();
697 }
698
699 fn wait_until(&self, deadline: std::time::Instant) {
702 let guard = self.lock.lock().unwrap();
703 let now = std::time::Instant::now();
704 if deadline <= now {
705 return;
706 }
707 let _ = self
708 .cond
709 .wait_timeout_while(guard, deadline - now, |_| true);
710 }
711}
712
713struct RenderState {
714 renderer: std::sync::Arc<Renderer>,
715 surface: &'static wgpu::Surface<'static>,
716 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
720 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
723 pacer: std::sync::Arc<LivePacer>,
726 current_bind: Option<wgpu::BindGroup>,
727 current_tex: Option<wgpu::Texture>,
728 width: u32,
729 height: u32,
730 current_width: u32,
731 current_height: u32,
732 format: wgpu::TextureFormat,
733 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
735 hw_accel: crate::video::HwAccel,
737 preload_frames: usize,
739 max_fps: Option<u32>,
741 scaling_mode: u32,
743 per_output_uniforms: std::sync::Arc<crate::renderer::PerOutputUniforms>,
745 last_wallpaper: Option<std::path::PathBuf>,
747 pre_blank: Option<(std::path::PathBuf, u32)>,
749 blanked: bool,
751 gif_paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
753}
754
755struct CommitData {
758 bg_bind: wgpu::BindGroup,
759 new_bind: wgpu::BindGroup,
760 img_width: u32,
761 img_height: u32,
762 old_img_width: u32,
763 old_img_height: u32,
764 format: wgpu::TextureFormat,
765 width: u32,
766 height: u32,
767 animated: Option<crate::animated::AnimatedImage>,
770 is_video: bool,
772 video_texture: Option<crate::renderer::VideoTexture>,
774 generation: u64,
777 scaling_mode: u32,
779 max_fps: Option<u32>,
781}
782
783impl RenderState {
784 fn set_wallpaper(
785 &mut self,
786 path: &std::path::Path,
787 effect: &crate::animation::Effect,
788 duration_ms: u32,
789 scaling_mode: u32,
790 ) -> anyhow::Result<()> {
791 let commit = self.commit_wallpaper(path, scaling_mode)?;
792 self.scaling_mode = scaling_mode;
793 self.spawn_transition(commit, effect, duration_ms);
794 self.last_wallpaper = Some(path.to_path_buf());
796 Ok(())
797 }
798
799 fn commit_wallpaper(
803 &mut self,
804 path: &std::path::Path,
805 scaling_mode: u32,
806 ) -> anyhow::Result<CommitData> {
807 use image::{ImageDecoder, ImageReader};
808
809 if crate::video::VideoDecoder::is_video_file(path) {
811 tracing::info!("Video file detected: {:?}", path);
812
813 let generation = self.playback_gen.load(Ordering::SeqCst).wrapping_add(1);
814 let renderer = self.renderer.clone();
815 let mut prepared = crate::video::VideoPlayback::prepare(
819 path,
820 self.hw_accel,
821 self.preload_frames,
822 std::time::Duration::from_millis(1000),
823 move |metadata| {
824 renderer
825 .validate_video_texture(metadata.width, metadata.height)
826 .map_err(crate::video::VideoError::GpuResourceCreation)
827 },
828 )?;
829 let metadata = prepared.metadata().clone();
830 let first_frame = prepared.take_first_frame();
831
832 let (tex_width, tex_height) = first_frame
833 .as_ref()
834 .map(|frame| (frame.width, frame.height))
835 .unwrap_or((metadata.width, metadata.height));
836 let video_texture = self.renderer.create_video_texture(tex_width, tex_height)?;
837 let (img_width, img_height) = if let Some(frame) = first_frame {
838 self.renderer
839 .update_video_texture(&video_texture, &frame.data)?;
840 (frame.width, frame.height)
841 } else {
842 tracing::warn!("No first frame available, using black texture");
844 (metadata.width, metadata.height)
845 };
846 self.video_playback.commit(prepared, generation);
847 self.playback_gen.store(generation, Ordering::SeqCst);
848 self.pacer.notify();
849 let new_tex = video_texture.texture().clone();
850 let new_bind = video_texture.bind_group().clone();
851
852 let old_bind = self.current_bind.take();
853 let (old_img_width, old_img_height) = if old_bind.is_some() {
854 (self.current_width.max(1), self.current_height.max(1))
855 } else {
856 (img_width, img_height)
857 };
858 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
859
860 drop(self.current_tex.take());
861 self.current_tex = Some(new_tex);
862 self.current_bind = Some(new_bind.clone());
863 self.current_width = img_width;
864 self.current_height = img_height;
865
866 return Ok(CommitData {
867 bg_bind,
868 new_bind,
869 img_width,
870 img_height,
871 old_img_width,
872 old_img_height,
873 format: self.format,
874 width: self.width,
875 height: self.height,
876 animated: None,
877 is_video: true,
878 video_texture: Some(video_texture),
879 generation,
880 scaling_mode,
881 max_fps: self.max_fps,
882 });
883 }
884
885 let mut animated = crate::animated::AnimatedImage::decode(path)?;
888 let (new_tex, new_bind, img_width, img_height) = if let Some(anim) = animated.as_mut() {
889 let (w, h) = (anim.width, anim.height);
890 let (tex, bind) = self.renderer.create_texture(w, h)?;
891 let first = anim.first_frame();
892 if !first.is_empty() {
893 self.renderer.update_texture(&tex, first, w, h);
894 }
895 (tex, bind, w, h)
896 } else {
897 let decoder = ImageReader::open(path)?.into_decoder()?;
898 let (source_width, source_height) = decoder.dimensions();
899 Renderer::validate_static_decode(source_width, source_height, decoder.total_bytes())?;
900 let new_img = image::DynamicImage::from_decoder(decoder)?;
901 let (tex, bind, width, height) =
902 self.renderer
903 .load_texture(&new_img, self.width, self.height, scaling_mode)?;
904 (tex, bind, width, height)
905 };
906
907 self.video_playback.stop();
910
911 let old_bind = self.current_bind.take();
912 let (old_img_width, old_img_height) = if old_bind.is_some() {
913 (self.current_width.max(1), self.current_height.max(1))
914 } else {
915 (img_width, img_height)
916 };
917 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
922
923 drop(self.current_tex.take());
924 self.current_tex = Some(new_tex);
925 self.current_bind = Some(new_bind.clone());
926 self.current_width = img_width;
927 self.current_height = img_height;
928
929 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
930 self.pacer.notify();
931
932 Ok(CommitData {
933 bg_bind,
934 new_bind,
935 img_width,
936 img_height,
937 old_img_width,
938 old_img_height,
939 format: self.format,
940 width: self.width,
941 height: self.height,
942 animated,
943 is_video: false,
944 video_texture: None,
945 generation,
946 scaling_mode,
947 max_fps: self.max_fps,
948 })
949 }
950
951 fn spawn_transition(
956 &self,
957 commit: CommitData,
958 effect: &crate::animation::Effect,
959 duration_ms: u32,
960 ) {
961 let renderer = self.renderer.clone();
962 let surface: &'static wgpu::Surface<'static> = self.surface;
963 let render_lock = self.render_lock.clone();
964 let playback_gen = self.playback_gen.clone();
965 let pacer = self.pacer.clone();
966 let video_playback = self.video_playback.clone();
967 let per_output_uniforms = std::sync::Arc::clone(&self.per_output_uniforms);
968 let gif_paused = self.gif_paused.clone();
969 let effect = effect.clone();
970 drop(tokio::task::spawn_blocking(move || {
971 render_transition(
972 renderer,
973 surface,
974 render_lock,
975 playback_gen,
976 pacer,
977 video_playback,
978 gif_paused,
979 commit,
980 effect,
981 duration_ms,
982 &per_output_uniforms,
983 );
984 }));
985 }
986}
987
988async fn restore_cached_wallpaper(name: &str, render_state: &Arc<Mutex<RenderState>>) {
989 let state_root = wallpaper_state_root();
990 let Some(path) = read_wallpaper_state(&state_root, "last_wallpaper", name) else {
991 return;
992 };
993
994 let effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
995 if let Err(err) = set_wallpaper_with_retry(render_state, &path, &effect, 1000, 0).await {
996 tracing::warn!("Failed to restore wallpaper for {name} from {path:?}: {err}");
997 let Some(previous) = read_wallpaper_state(&state_root, "previous_wallpaper", name) else {
998 return;
999 };
1000 match set_wallpaper_with_retry(render_state, &previous, &effect, 1000, 0).await {
1001 Ok(()) => {
1002 if let Err(persist_err) =
1003 write_wallpaper_state(&state_root, "last_wallpaper", name, &previous)
1004 {
1005 tracing::warn!(
1006 "Restored previous wallpaper for {name}, but failed to update state: {persist_err}"
1007 );
1008 } else {
1009 tracing::warn!("Restored previous wallpaper for {name} after {path:?} failed");
1010 }
1011 }
1012 Err(previous_err) => tracing::warn!(
1013 "Failed to restore previous wallpaper for {name} from {previous:?}: {previous_err}"
1014 ),
1015 }
1016 }
1017}
1018
1019const WALLPAPER_RETRY_ATTEMPTS: usize = 3;
1020const WALLPAPER_RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(150);
1021
1022async fn set_wallpaper_with_retry(
1023 render_state: &Arc<Mutex<RenderState>>,
1024 path: &std::path::Path,
1025 effect: &crate::animation::Effect,
1026 duration_ms: u32,
1027 scaling_mode: u32,
1028) -> anyhow::Result<()> {
1029 let render_state = Arc::clone(render_state);
1030 let path = path.to_path_buf();
1031 let effect = effect.clone();
1032
1033 tokio::task::spawn_blocking(move || {
1034 let mut attempt = 1;
1035 loop {
1036 let result = {
1037 let mut state = render_state.blocking_lock();
1038 state.set_wallpaper(&path, &effect, duration_ms, scaling_mode)
1039 };
1040 match result {
1041 Ok(()) => return Ok(()),
1042 Err(err)
1043 if attempt < WALLPAPER_RETRY_ATTEMPTS
1044 && is_transient_wallpaper_error(&err) =>
1045 {
1046 tracing::warn!(
1047 "Transient wallpaper error for {path:?} (attempt {attempt}/{WALLPAPER_RETRY_ATTEMPTS}): {err}"
1048 );
1049 attempt += 1;
1050 std::thread::sleep(WALLPAPER_RETRY_DELAY);
1051 }
1052 Err(err) => return Err(err),
1053 }
1054 }
1055 })
1056 .await?
1057}
1058
1059fn is_transient_wallpaper_error(err: &anyhow::Error) -> bool {
1060 err.chain().any(|cause| {
1061 cause
1062 .downcast_ref::<std::io::Error>()
1063 .is_some_and(|io_err| {
1064 matches!(
1065 io_err.kind(),
1066 std::io::ErrorKind::Interrupted
1067 | std::io::ErrorKind::WouldBlock
1068 | std::io::ErrorKind::TimedOut
1069 )
1070 })
1071 || cause
1072 .downcast_ref::<crate::video::VideoError>()
1073 .is_some_and(crate::video::VideoError::is_recoverable)
1074 })
1075}
1076
1077fn wallpaper_state_root() -> std::path::PathBuf {
1078 dirs::cache_dir()
1079 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1080 .join("wallr")
1081}
1082
1083fn wallpaper_state_path(root: &std::path::Path, state_dir: &str, name: &str) -> std::path::PathBuf {
1084 root.join(state_dir).join(name)
1085}
1086
1087fn read_wallpaper_state(
1088 root: &std::path::Path,
1089 state_dir: &str,
1090 name: &str,
1091) -> Option<std::path::PathBuf> {
1092 use std::os::unix::ffi::OsStringExt;
1093
1094 let path = std::fs::read(wallpaper_state_path(root, state_dir, name)).ok()?;
1095 let wallpaper = std::path::PathBuf::from(std::ffi::OsString::from_vec(path));
1096 (!wallpaper.as_os_str().is_empty()).then_some(wallpaper)
1097}
1098
1099fn write_wallpaper_state(
1100 root: &std::path::Path,
1101 state_dir: &str,
1102 name: &str,
1103 wallpaper: &std::path::Path,
1104) -> std::io::Result<()> {
1105 use std::io::Write;
1106 use std::os::unix::ffi::OsStrExt;
1107 use std::sync::atomic::{AtomicU64, Ordering};
1108
1109 static TEMP_ID: AtomicU64 = AtomicU64::new(0);
1110
1111 let state_path = wallpaper_state_path(root, state_dir, name);
1112 let parent = state_path
1113 .parent()
1114 .ok_or_else(|| std::io::Error::other("wallpaper state path has no parent"))?;
1115 std::fs::create_dir_all(parent)?;
1116 let file_name = state_path
1117 .file_name()
1118 .and_then(|value| value.to_str())
1119 .unwrap_or("wallpaper");
1120
1121 loop {
1122 let id = TEMP_ID.fetch_add(1, Ordering::Relaxed);
1123 let temporary_path = parent.join(format!(".{file_name}.{}.{}.tmp", std::process::id(), id));
1124 match std::fs::OpenOptions::new()
1125 .write(true)
1126 .create_new(true)
1127 .open(&temporary_path)
1128 {
1129 Ok(mut temporary) => {
1130 if let Err(err) = temporary
1131 .write_all(wallpaper.as_os_str().as_bytes())
1132 .and_then(|()| temporary.sync_all())
1133 .and_then(|()| std::fs::rename(&temporary_path, &state_path))
1134 {
1135 let _ = std::fs::remove_file(&temporary_path);
1136 return Err(err);
1137 }
1138 return Ok(());
1139 }
1140 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
1141 Err(err) => return Err(err),
1142 }
1143 }
1144}
1145
1146fn persist_wallpaper(name: &str, wallpaper: &std::path::Path) -> std::io::Result<()> {
1147 let root = wallpaper_state_root();
1148 persist_wallpaper_at(&root, name, wallpaper)
1149}
1150
1151fn persist_wallpaper_at(
1152 root: &std::path::Path,
1153 name: &str,
1154 wallpaper: &std::path::Path,
1155) -> std::io::Result<()> {
1156 let previous = read_wallpaper_state(root, "last_wallpaper", name)
1157 .filter(|current| current.exists() && current != wallpaper);
1158 write_wallpaper_state(root, "last_wallpaper", name, wallpaper)?;
1159 if let Some(previous) = previous {
1160 write_wallpaper_state(root, "previous_wallpaper", name, &previous)?;
1161 }
1162 Ok(())
1163}
1164
1165#[allow(clippy::too_many_arguments)]
1173fn render_transition(
1174 renderer: std::sync::Arc<Renderer>,
1175 surface: &'static wgpu::Surface<'static>,
1176 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
1177 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
1178 pacer: std::sync::Arc<LivePacer>,
1179 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
1180 gif_paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
1181 mut commit: CommitData,
1182 effect: crate::animation::Effect,
1183 duration_ms: u32,
1184 per_output_uniforms: &crate::renderer::PerOutputUniforms,
1185) {
1186 let _guard = render_lock
1187 .lock()
1188 .unwrap_or_else(|poisoned| poisoned.into_inner());
1189
1190 let duration = std::time::Duration::from_millis(u64::from(duration_ms.max(1)));
1191 let start = std::time::Instant::now();
1192 loop {
1193 let progress = start.elapsed().as_secs_f32() / duration.as_secs_f32();
1194 let uniforms = crate::animation::compute_effect_uniforms(&effect, progress.clamp(0.0, 1.0));
1195 let status = renderer.render_frame(
1196 crate::renderer::FrameRequest {
1197 surface,
1198 format: commit.format,
1199 bg_bind: &commit.bg_bind,
1200 new_bind: &commit.new_bind,
1201 effect: &uniforms,
1202 width: commit.width,
1203 height: commit.height,
1204 img_width: commit.img_width,
1205 img_height: commit.img_height,
1206 old_img_width: commit.old_img_width,
1207 old_img_height: commit.old_img_height,
1208 scaling_mode: commit.scaling_mode,
1209 },
1210 per_output_uniforms,
1211 );
1212 let status = match status {
1213 Ok(status) => status,
1214 Err(err) => {
1215 eprintln!("wallr: transition render failed: {err}");
1216 break;
1217 }
1218 };
1219 if progress >= 1.0 || status != crate::renderer::FrameStatus::Presented {
1220 break;
1221 }
1222 }
1223
1224 let mut animated = commit.animated.take();
1228 if let Some(animated) = animated.as_mut()
1229 && playback_gen.load(Ordering::SeqCst) == commit.generation
1230 {
1231 play_live(
1232 &renderer,
1233 surface,
1234 &commit,
1235 animated,
1236 &playback_gen,
1237 &pacer,
1238 &gif_paused,
1239 per_output_uniforms,
1240 );
1241 } else if commit.is_video && playback_gen.load(Ordering::SeqCst) == commit.generation {
1242 play_video(
1243 &renderer,
1244 surface,
1245 &commit,
1246 &video_playback,
1247 &playback_gen,
1248 &pacer,
1249 per_output_uniforms,
1250 );
1251 }
1252}
1253
1254#[allow(clippy::too_many_arguments)]
1260fn play_live(
1261 renderer: &Renderer,
1262 surface: &'static wgpu::Surface<'static>,
1263 commit: &CommitData,
1264 animated: &mut crate::animated::AnimatedImage,
1265 playback_gen: &std::sync::atomic::AtomicU64,
1266 pacer: &LivePacer,
1267 gif_paused: &std::sync::Arc<std::sync::atomic::AtomicBool>,
1268 per_output_uniforms: &crate::renderer::PerOutputUniforms,
1269) {
1270 let Ok((tex_a, bind_a)) = renderer.create_texture(animated.width, animated.height) else {
1271 tracing::warn!(
1272 "GIF dimensions {}x{} exceed the GPU texture limit",
1273 animated.width,
1274 animated.height
1275 );
1276 return;
1277 };
1278 let Ok((tex_b, bind_b)) = renderer.create_texture(animated.width, animated.height) else {
1279 tracing::warn!(
1280 "GIF dimensions {}x{} exceed the GPU texture limit",
1281 animated.width,
1282 animated.height
1283 );
1284 return;
1285 };
1286 let (frame_w, frame_h) = (animated.width, animated.height);
1287 let (bytes_per_row, rows) = (frame_w * 4, frame_h);
1288 let frame_bytes = bytes_per_row as u64 * rows as u64;
1289
1290 let direct_upload = bytes_per_row % 256 == 0;
1293 let staging: Vec<wgpu::Buffer> = if direct_upload {
1294 (0..2)
1295 .map(|_| {
1296 renderer.device.create_buffer(&wgpu::BufferDescriptor {
1297 label: Some("wallr-gif-staging"),
1298 size: frame_bytes,
1299 usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
1300 mapped_at_creation: false,
1301 })
1302 })
1303 .collect()
1304 } else {
1305 Vec::new()
1306 };
1307
1308 let first = animated.first_frame();
1309 if !first.is_empty() {
1310 renderer.update_texture(&tex_a, first, frame_w, frame_h);
1311 renderer.update_texture(&tex_b, first, frame_w, frame_h);
1312 }
1313 let binds = [bind_a, bind_b];
1314 let textures = [tex_a, tex_b];
1315
1316 let upload = |renderer: &Renderer,
1319 tgt: usize,
1320 index: usize,
1321 slot: usize,
1322 animated: &mut crate::animated::AnimatedImage|
1323 -> bool {
1324 if direct_upload {
1325 let buffer = &staging[slot];
1326 let slice = buffer.slice(..);
1327 slice.map_async(wgpu::MapMode::Write, |_| {});
1328 renderer.device.poll(wgpu::Maintain::Wait);
1329 let ok = {
1330 let mut mapped = slice.get_mapped_range_mut();
1331 animated.decompress_into(index, &mut mapped)
1332 };
1333 buffer.unmap();
1334 if ok {
1335 let mut encoder = renderer
1336 .device
1337 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
1338 encoder.copy_buffer_to_texture(
1339 wgpu::TexelCopyBufferInfo {
1340 buffer,
1341 layout: wgpu::TexelCopyBufferLayout {
1342 offset: 0,
1343 bytes_per_row: Some(bytes_per_row),
1344 rows_per_image: Some(rows),
1345 },
1346 },
1347 wgpu::TexelCopyTextureInfo {
1348 texture: &textures[tgt],
1349 mip_level: 0,
1350 origin: wgpu::Origin3d::ZERO,
1351 aspect: wgpu::TextureAspect::All,
1352 },
1353 wgpu::Extent3d {
1354 width: frame_w,
1355 height: frame_h,
1356 depth_or_array_layers: 1,
1357 },
1358 );
1359 renderer.queue.submit([encoder.finish()]);
1360 return true;
1361 }
1362 } else {
1363 let frame = animated.frame_at(index);
1364 if !frame.is_empty() {
1365 renderer.update_texture(&textures[tgt], frame, frame_w, frame_h);
1366 return true;
1367 }
1368 }
1369 false
1370 };
1371
1372 let mut cur = 0usize; let mut cur_frame = 0usize; let mut next_frame = 0usize; let mut slot = 0usize; let start = std::time::Instant::now();
1377 let mut paused_elapsed = std::time::Duration::ZERO; let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1379 loop {
1380 if playback_gen.load(Ordering::SeqCst) != commit.generation {
1381 return;
1382 }
1383
1384 if gif_paused.load(Ordering::SeqCst) {
1386 let pause_start = std::time::Instant::now();
1387 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
1389 let status = renderer.render_frame(
1390 crate::renderer::FrameRequest {
1391 surface,
1392 format: commit.format,
1393 bg_bind: &binds[cur],
1394 new_bind: &binds[cur],
1395 effect: &uniforms,
1396 width: commit.width,
1397 height: commit.height,
1398 img_width: animated.width,
1399 img_height: animated.height,
1400 old_img_width: animated.width,
1401 old_img_height: animated.height,
1402 scaling_mode: commit.scaling_mode,
1403 },
1404 per_output_uniforms,
1405 );
1406 match status {
1407 Ok(crate::renderer::FrameStatus::Presented) => {}
1408 _ => return,
1409 }
1410 std::thread::sleep(std::time::Duration::from_millis(16));
1412 paused_elapsed += pause_start.elapsed();
1413 continue;
1414 }
1415
1416 let index = animated.frame_index_at(start.elapsed() - paused_elapsed);
1417 if index != cur_frame {
1418 if next_frame != index {
1419 upload(renderer, cur ^ 1, index, slot, animated);
1420 slot ^= 1;
1421 next_frame = index;
1422 }
1423 cur ^= 1;
1424 cur_frame = index;
1425 }
1426 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
1427 let status = renderer.render_frame(
1428 crate::renderer::FrameRequest {
1429 surface,
1430 format: commit.format,
1431 bg_bind: &binds[cur],
1432 new_bind: &binds[cur],
1433 effect: &uniforms,
1434 width: commit.width,
1435 height: commit.height,
1436 img_width: animated.width,
1437 img_height: animated.height,
1438 old_img_width: animated.width,
1439 old_img_height: animated.height,
1440 scaling_mode: commit.scaling_mode,
1441 },
1442 per_output_uniforms,
1443 );
1444 match status {
1445 Ok(crate::renderer::FrameStatus::Presented) => {}
1446 _ => return,
1447 }
1448
1449 let elapsed = start.elapsed() - paused_elapsed;
1457 let total: std::time::Duration = animated.total_duration();
1458 let loops = (elapsed.as_millis() / total.as_millis().max(1)) as u64;
1459 let next_change = animated.frame_start(index + 1) + total * (loops as u32);
1460 let wait = next_change.saturating_sub(elapsed);
1461 if wait > std::time::Duration::ZERO {
1462 let next = index + 1;
1463 if next_frame != next {
1464 upload(renderer, cur ^ 1, next, slot, animated);
1465 slot ^= 1;
1466 next_frame = next;
1467 }
1468 pacer.wait_until(std::time::Instant::now() + wait);
1469 }
1470 }
1471}
1472
1473#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1474enum VideoPresentAction {
1475 Presented,
1476 Retry,
1477 Reconfigure,
1478}
1479
1480fn video_present_action(status: crate::renderer::FrameStatus) -> VideoPresentAction {
1481 match status {
1482 crate::renderer::FrameStatus::Presented => VideoPresentAction::Presented,
1483 crate::renderer::FrameStatus::TimedOut => VideoPresentAction::Retry,
1484 crate::renderer::FrameStatus::Outdated | crate::renderer::FrameStatus::Lost => {
1485 VideoPresentAction::Reconfigure
1486 }
1487 }
1488}
1489
1490fn play_video(
1492 renderer: &Renderer,
1493 surface: &'static wgpu::Surface<'static>,
1494 commit: &CommitData,
1495 video_playback: &std::sync::Arc<crate::video::VideoPlayback>,
1496 playback_gen: &std::sync::atomic::AtomicU64,
1497 pacer: &LivePacer,
1498 per_output_uniforms: &crate::renderer::PerOutputUniforms,
1499) {
1500 let (width, height) = (commit.img_width, commit.img_height);
1501
1502 let Some(texture) = commit.video_texture.as_ref() else {
1503 tracing::warn!("Video conversion resources unavailable");
1504 return;
1505 };
1506 let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1507
1508 let min_frame_interval = commit
1509 .max_fps
1510 .filter(|fps| *fps > 0)
1511 .map(|fps| std::time::Duration::from_secs_f64(1.0 / f64::from(fps)));
1512 let mut last_present: Option<std::time::Instant> = None;
1513 let mut warned_size_mismatch = false;
1514
1515 loop {
1516 if playback_gen.load(Ordering::SeqCst) != commit.generation {
1521 return;
1522 }
1523
1524 if let (Some(interval), Some(previous)) = (min_frame_interval, last_present) {
1525 pacer.wait_until(previous + interval);
1526 if playback_gen.load(Ordering::SeqCst) != commit.generation {
1527 return;
1528 }
1529 }
1530
1531 let frame_uploaded = if let Some(frame) =
1534 video_playback.next_frame_in_generation(commit.generation)
1535 {
1536 if frame.width != width || frame.height != height {
1539 if !warned_size_mismatch {
1540 tracing::warn!(
1541 "Skipping video frame with unexpected size {}x{} (expected {}x{})",
1542 frame.width,
1543 frame.height,
1544 width,
1545 height
1546 );
1547 warned_size_mismatch = true;
1548 }
1549 pacer.wait_until(std::time::Instant::now() + std::time::Duration::from_millis(2));
1550 continue;
1551 }
1552 if let Err(err) = renderer.update_video_texture(texture, &frame.data) {
1553 tracing::warn!("Video frame upload failed: {err}");
1554 return;
1555 }
1556 true
1557 } else {
1558 false
1559 };
1560
1561 if !frame_uploaded {
1562 if playback_gen.load(Ordering::SeqCst) != commit.generation {
1563 return;
1564 }
1565 let wait = video_playback
1566 .time_until_next_frame_in_generation(commit.generation)
1567 .unwrap_or(std::time::Duration::from_millis(2));
1568 if playback_gen.load(Ordering::SeqCst) != commit.generation {
1569 return;
1570 }
1571 pacer.wait_until(std::time::Instant::now() + wait);
1572 continue;
1573 }
1574
1575 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
1576 let status = renderer.render_frame(
1577 crate::renderer::FrameRequest {
1578 surface,
1579 format: commit.format,
1580 bg_bind: texture.bind_group(),
1581 new_bind: texture.bind_group(),
1582 effect: &uniforms,
1583 width: commit.width,
1584 height: commit.height,
1585 img_width: width,
1586 img_height: height,
1587 old_img_width: width,
1588 old_img_height: height,
1589 scaling_mode: commit.scaling_mode,
1590 },
1591 per_output_uniforms,
1592 );
1593
1594 match status.map(video_present_action) {
1595 Ok(VideoPresentAction::Presented) => {
1596 last_present = Some(std::time::Instant::now());
1597 }
1598 Ok(action) => {
1599 if action == VideoPresentAction::Reconfigure {
1600 surface.configure(
1601 &renderer.device,
1602 &wgpu::SurfaceConfiguration {
1603 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1604 format: commit.format,
1605 width: commit.width,
1606 height: commit.height,
1607 present_mode: wgpu::PresentMode::Fifo,
1608 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
1609 view_formats: vec![],
1610 desired_maximum_frame_latency: 2,
1611 },
1612 );
1613 }
1614 pacer.wait_until(std::time::Instant::now() + std::time::Duration::from_millis(100));
1615 }
1616 Err(err) => {
1617 tracing::warn!("Video present failed ({err}), stopping playback");
1618 video_playback.stop();
1619 return;
1620 }
1621 }
1622 }
1623}
1624
1625async fn resolve_targets(
1630 render_states: &std::collections::HashMap<
1631 String,
1632 std::sync::Arc<tokio::sync::Mutex<RenderState>>,
1633 >,
1634 monitor: Option<&str>,
1635) -> Vec<std::sync::Arc<tokio::sync::Mutex<RenderState>>> {
1636 match monitor {
1637 Some(name) => {
1638 if let Some(rs) = render_states.get(name) {
1639 vec![rs.clone()]
1640 } else {
1641 Vec::new()
1642 }
1643 }
1644 None => render_states.values().cloned().collect(),
1645 }
1646}
1647
1648fn resolve_named_targets(
1649 render_states: &std::collections::HashMap<
1650 String,
1651 std::sync::Arc<tokio::sync::Mutex<RenderState>>,
1652 >,
1653 monitor: Option<&str>,
1654) -> Vec<(String, std::sync::Arc<tokio::sync::Mutex<RenderState>>)> {
1655 match monitor {
1656 Some(name) => render_states
1657 .get(name)
1658 .map(|state| vec![(name.to_string(), state.clone())])
1659 .unwrap_or_default(),
1660 None => render_states
1661 .iter()
1662 .map(|(name, state)| (name.clone(), state.clone()))
1663 .collect(),
1664 }
1665}
1666
1667pub struct Daemon {
1668 config: WallrConfig,
1669 paused: Arc<AtomicBool>,
1670 engine: Arc<Mutex<WallpaperEngine>>,
1671}
1672
1673impl Daemon {
1674 pub fn new(config: WallrConfig) -> Result<Self, DaemonError> {
1675 let engine = WallpaperEngine::new(config.clone())?;
1676 Ok(Self {
1677 config,
1678 paused: Arc::new(AtomicBool::new(false)),
1679 engine: Arc::new(Mutex::new(engine)),
1680 })
1681 }
1682
1683 pub async fn start(self) -> Result<(), DaemonError> {
1684 let socket_path = crate::config::expand_path(&self.config.daemon.socket);
1685 if socket_path.exists() {
1686 if tokio::net::UnixStream::connect(&socket_path).await.is_ok() {
1687 return Err(DaemonError::AlreadyRunning(
1688 socket_path.to_string_lossy().to_string(),
1689 ));
1690 }
1691 let _ = std::fs::remove_file(&socket_path);
1692 }
1693
1694 let renderer = Renderer::new()
1695 .await
1696 .map_err(|e| DaemonError::StartError(format!("GPU init failed: {e}")))?;
1697
1698 let conn = Connection::connect_to_env()
1699 .map_err(|e| DaemonError::StartError(format!("Failed to connect to Wayland: {e:?}")))?;
1700 let backend = conn.backend();
1701 let display_ptr = backend.display_ptr() as *mut std::ffi::c_void;
1702
1703 let (globals, mut event_queue) = registry_queue_init(&conn)
1704 .map_err(|e| DaemonError::StartError(format!("registry_queue_init failed: {e:?}")))?;
1705 let qh = event_queue.handle();
1706
1707 let compositor_state = CompositorState::bind(&globals, &qh)
1708 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
1709 let layer_shell = LayerShell::bind(&globals, &qh)
1710 .map_err(|e| DaemonError::StartError(format!("layer_shell bind failed: {e:?}")))?;
1711 let shm = Shm::bind(&globals, &qh)
1712 .map_err(|e| DaemonError::StartError(format!("shm bind failed: {e:?}")))?;
1713
1714 let compositor = globals
1716 .bind::<wl_compositor::WlCompositor, WaylandState, smithay_client_toolkit::globals::GlobalData>(
1717 &qh,
1718 1..=4,
1719 smithay_client_toolkit::globals::GlobalData,
1720 )
1721 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
1722 let viewporter = globals
1723 .bind::<WpViewporter, WaylandState, ()>(&qh, 1..=1, ())
1724 .ok();
1725 if viewporter.is_none() {
1726 tracing::warn!(
1727 "wp_viewporter is unavailable; fractional outputs will use integer buffer scaling"
1728 );
1729 }
1730
1731 let mut wayland_state = WaylandState {
1732 registry_state: RegistryState::new(&globals),
1733 output_state: OutputState::new(&globals, &qh),
1734 compositor_state,
1735 shm,
1736 outputs: std::collections::HashMap::new(),
1737 surfaces: Vec::new(),
1738 viewporter,
1739 viewports: std::collections::HashMap::new(),
1740 output_lifecycles: std::collections::HashMap::new(),
1741 pending_restores: std::collections::HashSet::new(),
1742 layer_shell,
1743 compositor,
1744 hotplug: None,
1745 };
1746
1747 for i in 0..5 {
1751 event_queue
1752 .roundtrip(&mut wayland_state)
1753 .map_err(|e| DaemonError::StartError(format!("roundtrip {i} failed: {e:?}")))?;
1754 }
1755
1756 if wayland_state.outputs.is_empty() {
1757 return Err(DaemonError::StartError(
1758 "no outputs detected after roundtrip".into(),
1759 ));
1760 }
1761
1762 tracing::info!(
1763 "Detected {} output(s): {:?}",
1764 wayland_state.outputs.len(),
1765 wayland_state
1766 .outputs
1767 .values()
1768 .map(|o| format!("{} ({}x{})", o.name, o.width, o.height))
1769 .collect::<Vec<_>>()
1770 );
1771
1772 let renderer = std::sync::Arc::new(renderer);
1773
1774 let render_states_map: std::collections::HashMap<String, Arc<Mutex<RenderState>>> =
1778 std::collections::HashMap::new();
1779
1780 let output_info: Vec<(u32, OutputInfo)> = wayland_state
1783 .outputs
1784 .iter()
1785 .map(|(k, v)| {
1786 (
1787 *k,
1788 OutputInfo {
1789 name: v.name.clone(),
1790 width: v.width,
1791 height: v.height,
1792 scale_factor: v.scale_factor,
1793 wl_output: v.wl_output.clone(),
1794 },
1795 )
1796 })
1797 .collect();
1798
1799 for (proto_id, info) in &output_info {
1800 let name = info.name.clone();
1801 let rs = Self::create_render_state_for_output(
1802 &renderer,
1803 display_ptr,
1804 &mut wayland_state,
1805 &qh,
1806 info,
1807 &self.config,
1808 )
1809 .await?;
1810 let rs = Arc::new(Mutex::new(rs));
1811 wayland_state.output_lifecycles.insert(
1812 *proto_id,
1813 OutputLifecycle {
1814 name: name.clone(),
1815 render_state: rs,
1816 active: Arc::new(std::sync::atomic::AtomicBool::new(true)),
1817 },
1818 );
1819 wayland_state.pending_restores.insert(*proto_id);
1820
1821 tracing::info!("Output ready: {name} ({proto_id})");
1822 }
1823
1824 let render_states: std::sync::Arc<
1827 tokio::sync::Mutex<std::collections::HashMap<String, Arc<Mutex<RenderState>>>>,
1828 > = std::sync::Arc::new(tokio::sync::Mutex::new(render_states_map));
1829
1830 wayland_state.hotplug = Some(DaemonHotplug {
1833 renderer: renderer.clone(),
1834 config: self.config.clone(),
1835 display_ptr: SendDisplayPtr(display_ptr),
1836 render_states: render_states.clone(),
1837 });
1838
1839 event_queue
1842 .roundtrip(&mut wayland_state)
1843 .map_err(|e| DaemonError::StartError(format!("hotplug roundtrip failed: {e:?}")))?;
1844
1845 let paused_clone = self.paused.clone();
1846 let engine_clone = self.engine.clone();
1847 let render_states_clone = render_states.clone();
1848
1849 {
1853 let rs_map = render_states.clone();
1854 let socket_path = socket_path.clone();
1855 tokio::spawn(async move {
1856 use tokio::signal::unix::{SignalKind, signal};
1857 let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler");
1858 let mut int = signal(SignalKind::interrupt()).expect("SIGINT handler");
1859 let mut hup = signal(SignalKind::hangup()).expect("SIGHUP handler");
1860 tokio::select! {
1861 _ = term.recv() => {}
1862 _ = int.recv() => {}
1863 _ = hup.recv() => {}
1864 }
1865 tracing::info!("Signal received, shutting down gracefully");
1866 let states = rs_map.lock().await;
1867 for rs in states.values() {
1868 if let Ok(state) = rs.try_lock() {
1869 state.video_playback.stop();
1870 }
1871 }
1872 drop(states);
1873 let _ = std::fs::remove_file(&socket_path);
1874 std::process::exit(0);
1875 });
1876 }
1877
1878 let ipc_socket_path = socket_path.clone();
1879 start_ipc_server(&socket_path, move |cmd| {
1880 let paused = paused_clone.clone();
1881 let engine = engine_clone.clone();
1882 let render_states = render_states_clone.clone();
1883 let stop_socket = ipc_socket_path.clone();
1884 async move {
1885 let render_states = render_states.lock().await;
1886 match cmd {
1887 IpcCommand::Pause { monitor } => {
1888 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1889 if targets.is_empty() {
1890 return IpcResponse {
1891 success: false,
1892 message: Some("No matching outputs".into()),
1893 };
1894 }
1895 if monitor.is_none() {
1896 paused.store(true, Ordering::SeqCst);
1897 }
1898 for rs in targets {
1899 let rs_lock = rs.lock().await;
1900 rs_lock.video_playback.pause();
1901 rs_lock.gif_paused.store(true, Ordering::SeqCst);
1902 }
1903 IpcResponse {
1904 success: true,
1905 message: Some("Paused".into()),
1906 }
1907 }
1908 IpcCommand::Resume { monitor } => {
1909 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1910 if targets.is_empty() {
1911 return IpcResponse {
1912 success: false,
1913 message: Some("No matching outputs".into()),
1914 };
1915 }
1916 if monitor.is_none() {
1917 paused.store(false, Ordering::SeqCst);
1918 }
1919 for rs in targets {
1920 let rs_lock = rs.lock().await;
1921 rs_lock.video_playback.resume();
1922 rs_lock.gif_paused.store(false, Ordering::SeqCst);
1923 }
1924 IpcResponse {
1925 success: true,
1926 message: Some("Resumed".into()),
1927 }
1928 }
1929 IpcCommand::Reload => {
1930 let lock = engine.lock().await;
1931 match lock.reload() {
1932 Ok(_) => IpcResponse {
1933 success: true,
1934 message: Some("Reloaded".into()),
1935 },
1936 Err(e) => IpcResponse {
1937 success: false,
1938 message: Some(e.to_string()),
1939 },
1940 }
1941 }
1942 IpcCommand::Preview {
1943 path,
1944 effect,
1945 duration_ms,
1946 no_theme,
1947 theme_override,
1948 monitor,
1949 scaling_mode,
1950 } => {
1951 if paused.load(Ordering::SeqCst) {
1952 return IpcResponse {
1953 success: false,
1954 message: Some("Daemon is paused".into()),
1955 };
1956 }
1957 let p = std::path::PathBuf::from(&path);
1958 if !p.exists() {
1959 return IpcResponse {
1960 success: false,
1961 message: Some(format!("File not found: {}", path)),
1962 };
1963 }
1964
1965 let targets = resolve_named_targets(&render_states, monitor.as_deref());
1967 if targets.is_empty() {
1968 return IpcResponse {
1969 success: false,
1970 message: match &monitor {
1971 Some(name) => Some(format!("Unknown monitor: {name}")),
1972 None => Some("No outputs available".into()),
1973 },
1974 };
1975 }
1976
1977 let effect = effect.unwrap_or_else(|| {
1978 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
1979 });
1980 let is_video = crate::video::VideoDecoder::is_video_file(&p);
1984 let duration = duration_ms.unwrap_or(if is_video { 150 } else { 2000 });
1985 let sm = scaling_mode.unwrap_or(crate::config::ScalingMode::Fill);
1986 let scaling_mode_u32 = match sm {
1987 crate::config::ScalingMode::Fill => 0u32,
1988 crate::config::ScalingMode::Fit => 1,
1989 crate::config::ScalingMode::Stretch => 2,
1990 crate::config::ScalingMode::Center => 3,
1991 crate::config::ScalingMode::Tile => 4,
1992 };
1993
1994 let mut last_err = None;
1995 for (name, rs) in &targets {
1996 let result = set_wallpaper_with_retry(
1997 rs,
1998 &p,
1999 &effect,
2000 duration,
2001 scaling_mode_u32,
2002 )
2003 .await;
2004
2005 match result {
2006 Err(e) => last_err = Some(format!("Render failed: {e}")),
2007 Ok(()) => {
2008 if let Err(err) = persist_wallpaper(name, &p) {
2009 tracing::warn!(
2010 "Wallpaper changed on {name}, but state persistence failed: {err}"
2011 );
2012 }
2013 }
2014 }
2015 }
2016
2017 match last_err {
2018 Some(e) => IpcResponse {
2019 success: false,
2020 message: Some(e),
2021 },
2022 None => {
2023 let opts = SetOptions {
2024 no_theme,
2025 theme_provider: theme_override,
2026 monitor,
2027 };
2028 let mut eng = engine.lock().await;
2029 match eng.set_wallpaper(&p, &opts).await {
2030 Ok(()) => IpcResponse {
2031 success: true,
2032 message: None,
2033 },
2034 Err(e) => IpcResponse {
2035 success: true,
2036 message: Some(format!(
2037 "Wallpaper set, but hooks/theme failed: {e}"
2038 )),
2039 },
2040 }
2041 }
2042 }
2043 }
2044 IpcCommand::Stop => {
2045 for rs in render_states.values() {
2046 let state = rs.lock().await;
2047 state.playback_gen.fetch_add(1, Ordering::SeqCst);
2048 state.pacer.notify();
2049 state.video_playback.stop();
2050 }
2051 let sp = stop_socket.clone();
2052 tokio::spawn(async move {
2053 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
2054 let _ = std::fs::remove_file(&sp);
2055 std::process::exit(0);
2056 });
2057 IpcResponse {
2058 success: true,
2059 message: Some("Stopping".into()),
2060 }
2061 }
2062 IpcCommand::Status => {
2063 let state = if paused.load(Ordering::SeqCst) {
2064 "paused"
2065 } else {
2066 "running"
2067 };
2068 IpcResponse {
2069 success: true,
2070 message: Some(format!("wallr daemon {}", state)),
2071 }
2072 }
2073 IpcCommand::Seek {
2074 timestamp_ms,
2075 monitor,
2076 } => {
2077 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
2078 if targets.is_empty() {
2079 return IpcResponse {
2080 success: false,
2081 message: match &monitor {
2082 Some(name) => Some(format!("Unknown monitor: {name}")),
2083 None => Some("No outputs available".into()),
2084 },
2085 };
2086 }
2087 let mut seek_count = 0u32;
2089 let mut errors = Vec::new();
2090 for (name, rs) in render_states.iter() {
2091 if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
2092 continue;
2093 }
2094 let rs_lock = rs.lock().await;
2095 match rs_lock
2096 .video_playback
2097 .seek(std::time::Duration::from_millis(timestamp_ms))
2098 {
2099 Ok(()) => {
2100 seek_count += 1;
2101 }
2102 Err(e) => {
2103 errors.push(format!("{}: {}", name, e));
2104 }
2105 }
2106 }
2107 if seek_count == 0 {
2108 IpcResponse {
2109 success: false,
2110 message: Some(format!(
2111 "Seek failed on all outputs: {}",
2112 errors.join("; ")
2113 )),
2114 }
2115 } else if !errors.is_empty() {
2116 IpcResponse {
2117 success: true,
2118 message: Some(format!(
2119 "Seeked {} output(s) to {}ms, {} failed: {}",
2120 seek_count,
2121 timestamp_ms,
2122 errors.len(),
2123 errors.join("; ")
2124 )),
2125 }
2126 } else {
2127 IpcResponse {
2128 success: true,
2129 message: Some(format!(
2130 "Seeked {} output(s) to {}ms",
2131 seek_count, timestamp_ms
2132 )),
2133 }
2134 }
2135 }
2136 IpcCommand::Info { monitor } => {
2137 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
2138 if targets.is_empty() {
2139 return IpcResponse {
2140 success: false,
2141 message: match &monitor {
2142 Some(name) => Some(format!("Unknown monitor: {name}")),
2143 None => Some("No outputs available".into()),
2144 },
2145 };
2146 }
2147
2148 let mut lines = vec![
2149 format!("wallr v{}", env!("CARGO_PKG_VERSION")),
2150 String::new(),
2151 format!("Outputs: {}", render_states.len()),
2152 ];
2153 for name in render_states.keys() {
2154 lines.push(format!(" - {name}"));
2155 }
2156
2157 for (name, rs) in render_states.iter() {
2159 if monitor.is_some() && monitor.as_deref() != Some(name.as_str()) {
2160 continue;
2161 }
2162 let rs_lock = rs.lock().await;
2163 let gpu_info =
2164 crate::video::gpu::adapter_diagnostics(&rs_lock.renderer.adapter);
2165
2166 lines.push(String::new());
2167 lines.push(format!("[{name}] {}x{}", rs_lock.width, rs_lock.height));
2168 lines.push(gpu_info);
2169
2170 match rs_lock.video_playback.metadata() {
2171 Some(meta) => {
2172 let decoder_info = rs_lock.video_playback.decoder_info();
2173 let hw = rs_lock.video_playback.hw_accel_in_use();
2174 let state = if rs_lock.video_playback.is_paused() {
2175 "paused"
2176 } else {
2177 "playing"
2178 };
2179 let position = rs_lock
2180 .video_playback
2181 .position()
2182 .map(|p| format!("{:.2}s", p.as_secs_f64()))
2183 .unwrap_or_else(|| "?".to_string());
2184 lines.push(String::new());
2185 lines.push("Video:".into());
2186 lines.push(format!(
2187 " Resolution: {}x{}",
2188 meta.width, meta.height
2189 ));
2190 lines.push(format!(" FPS: {:.2}", meta.fps));
2191 lines.push(format!(
2192 " Duration: {:.2}s",
2193 meta.duration.as_secs_f64()
2194 ));
2195 lines.push(format!(
2196 " Codec: {}",
2197 decoder_info
2198 .as_ref()
2199 .map(|d| d.codec_name.as_str())
2200 .unwrap_or("unknown")
2201 ));
2202 lines.push(format!(" Container: {}", meta.format));
2203 lines.push(format!(" Decoder: {}", hw.name()));
2204 lines.push(format!(
2205 " GPU Decode: {}",
2206 if hw == crate::video::HwAccel::Software {
2207 "disabled"
2208 } else {
2209 "enabled"
2210 }
2211 ));
2212 lines.push(format!(" State: {} @ {}", state, position));
2213 }
2214 None => {
2215 lines.push(String::new());
2216 lines.push("Video: none active".into());
2217 lines.push("Decoder: idle".into());
2218 }
2219 }
2220 }
2221
2222 IpcResponse {
2223 success: true,
2224 message: Some(lines.join("\n")),
2225 }
2226 }
2227 IpcCommand::MonitorList => {
2228 let mut lines = Vec::new();
2229 for (name, rs) in render_states.iter() {
2230 let lock = rs.lock().await;
2231 lines.push(format!("{}: {}x{}", name, lock.width, lock.height));
2232 }
2233 if lines.is_empty() {
2234 IpcResponse {
2235 success: true,
2236 message: Some("No monitors connected".into()),
2237 }
2238 } else {
2239 IpcResponse {
2240 success: true,
2241 message: Some(lines.join("\n")),
2242 }
2243 }
2244 }
2245 IpcCommand::MonitorCurrent => {
2246 if let Some((name, rs)) = render_states.iter().next() {
2248 let lock = rs.lock().await;
2249 IpcResponse {
2250 success: true,
2251 message: Some(format!("{}: {}x{}", name, lock.width, lock.height)),
2252 }
2253 } else {
2254 IpcResponse {
2255 success: false,
2256 message: Some("No monitors connected".into()),
2257 }
2258 }
2259 }
2260 IpcCommand::Blank {
2261 monitor,
2262 effect,
2263 duration_ms,
2264 } => {
2265 let targets = resolve_named_targets(&render_states, monitor.as_deref());
2266 if targets.is_empty() {
2267 return IpcResponse {
2268 success: false,
2269 message: match &monitor {
2270 Some(name) => Some(format!("Unknown monitor: {name}")),
2271 None => Some("No outputs available".into()),
2272 },
2273 };
2274 }
2275 let mut blanked_count = 0u32;
2276 let mut errors = Vec::new();
2277 let black_effect = effect.unwrap_or_else(|| {
2278 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
2279 });
2280 let duration = duration_ms.unwrap_or(800);
2281 let mut blank_file = match tempfile::Builder::new()
2282 .prefix("wallr_blank-")
2283 .suffix(".png")
2284 .tempfile()
2285 {
2286 Ok(file) => file,
2287 Err(e) => {
2288 return IpcResponse {
2289 success: false,
2290 message: Some(format!("Failed to create blank image: {e}")),
2291 };
2292 }
2293 };
2294 let blank_path = blank_file.path().to_path_buf();
2295 let blank = image::DynamicImage::ImageRgba8(
2296 image::RgbaImage::from_pixel(1, 1, image::Rgba([0, 0, 0, 255])),
2297 );
2298 if let Err(e) = blank
2299 .write_to(blank_file.as_file_mut(), image::ImageFormat::Png)
2300 {
2301 return IpcResponse {
2302 success: false,
2303 message: Some(format!("Failed to write blank image: {e}")),
2304 };
2305 }
2306
2307 for (name, rs) in &targets {
2308 let rs = Arc::clone(rs);
2309 let blank_path = blank_path.clone();
2310 let black_effect = black_effect.clone();
2311 let blanked = tokio::task::spawn_blocking(move || {
2312 let mut lock = rs.blocking_lock();
2313 if lock.blanked {
2314 return Ok(false);
2315 }
2316 let previous = (
2317 lock.last_wallpaper.clone().unwrap_or_default(),
2318 lock.scaling_mode,
2319 );
2320 lock.set_wallpaper(&blank_path, &black_effect, duration, 0)?;
2321 lock.pre_blank = Some(previous);
2322 lock.blanked = true;
2323 Ok::<bool, anyhow::Error>(true)
2324 })
2325 .await;
2326 match blanked {
2327 Ok(Ok(true)) => blanked_count += 1,
2328 Ok(Ok(false)) => {}
2329 Ok(Err(e)) => errors.push(format!("{name}: blank failed: {e}")),
2330 Err(e) => errors.push(format!("{name}: blank task failed: {e}")),
2331 }
2332 }
2333 if errors.is_empty() {
2334 IpcResponse {
2335 success: true,
2336 message: Some(format!("Blanked {blanked_count} output(s)")),
2337 }
2338 } else {
2339 IpcResponse {
2340 success: false,
2341 message: Some(format!(
2342 "Blanked {blanked_count} output(s), {} error(s): {}",
2343 errors.len(),
2344 errors.join("; ")
2345 )),
2346 }
2347 }
2348 }
2349 IpcCommand::Restore {
2350 monitor,
2351 effect,
2352 duration_ms,
2353 } => {
2354 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
2355 if targets.is_empty() {
2356 return IpcResponse {
2357 success: false,
2358 message: match &monitor {
2359 Some(name) => Some(format!("Unknown monitor: {name}")),
2360 None => Some("No outputs available".into()),
2361 },
2362 };
2363 }
2364 let mut restored_count = 0u32;
2365 let mut errors = Vec::new();
2366 let restore_effect = effect.unwrap_or_else(|| {
2367 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
2368 });
2369 let duration = duration_ms.unwrap_or(800);
2370
2371 for (name, rs) in render_states.iter() {
2372 if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
2373 continue;
2374 }
2375 let rs = Arc::clone(rs);
2376 let restore_effect = restore_effect.clone();
2377 let result = tokio::task::spawn_blocking(move || {
2378 let mut lock = rs.blocking_lock();
2379 if !lock.blanked {
2380 return None;
2381 }
2382 let result = match lock.pre_blank.clone() {
2383 Some((path, scaling_mode)) if path.exists() => lock
2384 .set_wallpaper(
2385 &path,
2386 &restore_effect,
2387 duration,
2388 scaling_mode,
2389 )
2390 .map_err(|e| format!("restore failed: {e}")),
2391 Some(_) => Err("wallpaper path no longer exists".to_string()),
2392 None => Err("no previous wallpaper to restore".to_string()),
2393 };
2394 if result.is_ok() {
2395 lock.blanked = false;
2396 lock.pre_blank = None;
2397 }
2398 Some(result)
2399 })
2400 .await;
2401 match result {
2402 Ok(Some(Ok(()))) => restored_count += 1,
2403 Ok(Some(Err(e))) => errors.push(format!("{name}: {e}")),
2404 Ok(None) => {}
2405 Err(e) => errors.push(format!("{name}: restore task failed: {e}")),
2406 }
2407 }
2408 if !errors.is_empty() {
2409 IpcResponse {
2410 success: restored_count > 0,
2411 message: Some(format!(
2412 "Restored {} output(s), {} error(s): {}",
2413 restored_count,
2414 errors.len(),
2415 errors.join("; ")
2416 )),
2417 }
2418 } else {
2419 IpcResponse {
2420 success: true,
2421 message: Some(format!("Restored {restored_count} output(s)")),
2422 }
2423 }
2424 }
2425 }
2426 }
2427 })
2428 .await?;
2429
2430 if self.config.watch.enabled
2432 && let Some(ref watch_dir) = self.config.watch.dir
2433 {
2434 let watch_path = crate::config::expand_path(watch_dir);
2435 self.start_watcher(watch_path, render_states.clone())
2436 .await?;
2437 }
2438
2439 tokio::task::spawn_blocking(move || {
2440 loop {
2441 if let Err(e) = event_queue.blocking_dispatch(&mut wayland_state) {
2442 eprintln!("Wayland dispatch error: {e:?}");
2443 break;
2444 }
2445 }
2446 eprintln!("wallr: Wayland connection lost, exiting");
2450 std::process::exit(1);
2451 });
2452
2453 loop {
2454 tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
2455 }
2456 }
2457
2458 async fn start_watcher(
2459 &self,
2460 dir: PathBuf,
2461 render_states: std::sync::Arc<
2462 tokio::sync::Mutex<std::collections::HashMap<String, Arc<Mutex<RenderState>>>>,
2463 >,
2464 ) -> Result<(), DaemonError> {
2465 let engine = self.engine.clone();
2466 let paused = self.paused.clone();
2467 let debounce = crate::config::parse_duration(&self.config.watch.debounce)
2468 .unwrap_or(std::time::Duration::from_millis(500));
2469
2470 let (tx, mut rx) = tokio::sync::mpsc::channel(100);
2471
2472 let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
2473 if let Ok(event) = res
2474 && let EventKind::Create(_) = event.kind
2475 {
2476 for path in event.paths {
2477 let _ = tx.blocking_send(path);
2478 }
2479 }
2480 })
2481 .map_err(|e| DaemonError::StartError(e.to_string()))?;
2482
2483 watcher
2484 .watch(&dir, RecursiveMode::NonRecursive)
2485 .map_err(|e| DaemonError::StartError(e.to_string()))?;
2486
2487 tokio::spawn(async move {
2488 let _watcher = watcher;
2489 let mut last: Option<(PathBuf, std::time::Instant)> = None;
2490
2491 while let Some(path) = rx.recv().await {
2492 if paused.load(Ordering::SeqCst) {
2493 continue;
2494 }
2495 if let Some((ref lp, ref lt)) = last
2496 && lp == &path
2497 && lt.elapsed() < debounce
2498 {
2499 continue;
2500 }
2501 let ext = path
2502 .extension()
2503 .unwrap_or_default()
2504 .to_string_lossy()
2505 .to_lowercase();
2506 if !["jpg", "jpeg", "png", "gif", "webp"].contains(&ext.as_str()) {
2507 continue;
2508 }
2509 last = Some((path.clone(), std::time::Instant::now()));
2510
2511 let states = render_states.lock().await;
2513 for (name, rs) in states.iter() {
2514 let rs = rs.clone();
2515 let eng = engine.clone();
2516 let p = path.clone();
2517 let name = name.clone();
2518 tokio::spawn(async move {
2519 let effect =
2520 crate::animation::Effect::Fade(crate::animation::FadeParams::default());
2521 if let Err(err) = set_wallpaper_with_retry(&rs, &p, &effect, 600, 0).await {
2522 tracing::warn!("Watcher wallpaper render failed for {p:?}: {err}");
2523 return;
2524 }
2525 let opts = SetOptions {
2526 no_theme: false,
2527 theme_provider: None,
2528 monitor: Some(name),
2529 };
2530 let mut elock = eng.lock().await;
2531 let _ = elock.set_wallpaper(&p, &opts).await;
2532 });
2533 }
2534 }
2535 });
2536
2537 Ok(())
2538 }
2539
2540 #[allow(clippy::too_many_arguments)]
2543 async fn create_render_state_for_output(
2544 renderer: &std::sync::Arc<Renderer>,
2545 display_ptr: *mut std::ffi::c_void,
2546 wayland_state: &mut WaylandState,
2547 qh: &QueueHandle<WaylandState>,
2548 output: &OutputInfo,
2549 config: &WallrConfig,
2550 ) -> Result<RenderState, DaemonError> {
2551 let wl_surface = wayland_state.compositor_state.create_surface(qh);
2552 let layer_surface = wayland_state.layer_shell.create_layer_surface(
2553 qh,
2554 wl_surface,
2555 Layer::Background,
2556 Some("wallr"),
2557 Some(&output.wl_output),
2558 );
2559 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
2560 layer_surface.set_exclusive_zone(-1);
2561 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
2562
2563 let empty_region = wayland_state.compositor.create_region(qh, ());
2565 layer_surface
2566 .wl_surface()
2567 .set_input_region(Some(&empty_region));
2568 let output_id = output.wl_output.id().protocol_id();
2569 let viewport = wayland_state
2570 .viewporter
2571 .as_ref()
2572 .map(|viewporter| viewporter.get_viewport(layer_surface.wl_surface(), qh, ()));
2573 let scale_factor = if viewport.is_some() {
2574 1
2575 } else if output.scale_factor > 0 {
2576 output.scale_factor
2577 } else {
2578 1
2579 };
2580 layer_surface.wl_surface().set_buffer_scale(scale_factor);
2581 layer_surface.commit();
2582 empty_region.destroy();
2583 if let Some(viewport) = viewport {
2584 wayland_state.viewports.insert(output_id, viewport);
2585 }
2586
2587 let width = output.width;
2589 let height = output.height;
2590
2591 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
2592 wayland_state.surfaces.push((output_id, layer_surface));
2593
2594 let window_handle = WaylandWindow {
2595 display: display_ptr,
2596 surface: raw_surface,
2597 };
2598
2599 let wgpu_surface = renderer
2600 .instance
2601 .create_surface(&window_handle)
2602 .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
2603
2604 let adapter = renderer
2605 .instance
2606 .request_adapter(&wgpu::RequestAdapterOptions {
2607 compatible_surface: Some(&wgpu_surface),
2608 power_preference: wgpu::PowerPreference::HighPerformance,
2609 force_fallback_adapter: false,
2610 })
2611 .await;
2612 let surf_format = adapter
2613 .as_ref()
2614 .map(|a| {
2615 let caps = wgpu_surface.get_capabilities(a);
2616 caps.formats
2617 .into_iter()
2618 .next()
2619 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
2620 })
2621 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb);
2622
2623 let surf_config = wgpu::SurfaceConfiguration {
2624 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2625 format: surf_format,
2626 width,
2627 height,
2628 present_mode: wgpu::PresentMode::Fifo,
2629 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
2630 view_formats: vec![],
2631 desired_maximum_frame_latency: 2,
2632 };
2633 wgpu_surface.configure(&renderer.device, &surf_config);
2634
2635 let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
2638 let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
2639
2640 Ok(RenderState {
2641 renderer: renderer.clone(),
2642 surface,
2643 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
2644 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
2645 pacer: std::sync::Arc::new(LivePacer::new()),
2646 current_bind: None,
2647 current_tex: None,
2648 width,
2649 height,
2650 current_width: 0,
2651 current_height: 0,
2652 format: surf_format,
2653 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
2654 hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
2655 preload_frames: config.video.preload_frames,
2656 max_fps: config.daemon.max_fps,
2657 scaling_mode: 0,
2658 per_output_uniforms: std::sync::Arc::new(renderer.create_per_output_uniforms()),
2659 last_wallpaper: None,
2660 pre_blank: None,
2661 blanked: false,
2662 gif_paused: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2663 })
2664 }
2665}
2666
2667#[allow(clippy::too_many_arguments)]
2671fn create_render_state_for_output_sync(
2672 renderer: &std::sync::Arc<Renderer>,
2673 display_ptr: *mut std::ffi::c_void,
2674 wayland_state: &mut WaylandState,
2675 qh: &QueueHandle<WaylandState>,
2676 output: &OutputInfo,
2677 config: &WallrConfig,
2678) -> Result<RenderState, DaemonError> {
2679 let wl_surface = wayland_state.compositor_state.create_surface(qh);
2680 let layer_surface = wayland_state.layer_shell.create_layer_surface(
2681 qh,
2682 wl_surface,
2683 Layer::Background,
2684 Some("wallr"),
2685 Some(&output.wl_output),
2686 );
2687 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
2688 layer_surface.set_exclusive_zone(-1);
2689 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
2690
2691 let empty_region = wayland_state.compositor.create_region(qh, ());
2692 layer_surface
2693 .wl_surface()
2694 .set_input_region(Some(&empty_region));
2695 let output_id = output.wl_output.id().protocol_id();
2696 let viewport = wayland_state
2697 .viewporter
2698 .as_ref()
2699 .map(|viewporter| viewporter.get_viewport(layer_surface.wl_surface(), qh, ()));
2700 let scale_factor = if viewport.is_some() {
2701 1
2702 } else if output.scale_factor > 0 {
2703 output.scale_factor
2704 } else {
2705 1
2706 };
2707 layer_surface.wl_surface().set_buffer_scale(scale_factor);
2708 layer_surface.commit();
2709 empty_region.destroy();
2710 if let Some(viewport) = viewport {
2711 wayland_state.viewports.insert(output_id, viewport);
2712 }
2713
2714 let width = output.width;
2716 let height = output.height;
2717
2718 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
2719 wayland_state.surfaces.push((output_id, layer_surface));
2720
2721 let window_handle = WaylandWindow {
2722 display: display_ptr,
2723 surface: raw_surface,
2724 };
2725
2726 let wgpu_surface = renderer
2727 .instance
2728 .create_surface(&window_handle)
2729 .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
2730
2731 let surf_format = {
2732 let caps = wgpu_surface.get_capabilities(&renderer.adapter);
2733 caps.formats
2734 .into_iter()
2735 .next()
2736 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
2737 };
2738
2739 let surf_config = wgpu::SurfaceConfiguration {
2740 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2741 format: surf_format,
2742 width,
2743 height,
2744 present_mode: wgpu::PresentMode::Fifo,
2745 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
2746 view_formats: vec![],
2747 desired_maximum_frame_latency: 2,
2748 };
2749 wgpu_surface.configure(&renderer.device, &surf_config);
2750
2751 let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
2752 let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
2753
2754 Ok(RenderState {
2755 renderer: renderer.clone(),
2756 surface,
2757 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
2758 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
2759 pacer: std::sync::Arc::new(LivePacer::new()),
2760 current_bind: None,
2761 current_tex: None,
2762 width,
2763 height,
2764 current_width: 0,
2765 current_height: 0,
2766 format: surf_format,
2767 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
2768 hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
2769 preload_frames: config.video.preload_frames,
2770 max_fps: config.daemon.max_fps,
2771 scaling_mode: 0,
2772 per_output_uniforms: std::sync::Arc::new(renderer.create_per_output_uniforms()),
2773 last_wallpaper: None,
2774 pre_blank: None,
2775 blanked: false,
2776 gif_paused: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2777 })
2778}