1use crate::config::WallrConfig;
2use crate::ipc::{IpcCommand, IpcResponse, IpcSocket, remove_socket_if_owned};
3use crate::renderer::Renderer;
4use crate::wallpaper::{SetOptions, WallpaperEngine};
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7use tokio::sync::Mutex;
8
9use raw_window_handle::{
10 DisplayHandle, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle,
11 WaylandDisplayHandle, WaylandWindowHandle, WindowHandle,
12};
13use smithay_client_toolkit::{
14 compositor::{CompositorHandler, CompositorState},
15 delegate_registry,
16 output::{OutputHandler, OutputState},
17 registry::{ProvidesRegistryState, RegistryState},
18 registry_handlers,
19 shell::WaylandSurface,
20 shell::wlr_layer::{
21 Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
22 LayerSurfaceConfigure,
23 },
24 shm::slot::{Buffer as ShmBuffer, SlotPool},
25 shm::{Shm, ShmHandler},
26};
27use wayland_client::{
28 Connection, Dispatch, Proxy, QueueHandle,
29 globals::registry_queue_init,
30 protocol::{wl_compositor, wl_output, wl_surface},
31};
32use wayland_protocols::wp::viewporter::client::{
33 wp_viewport::{self, WpViewport},
34 wp_viewporter::{self, WpViewporter},
35};
36#[derive(Debug, thiserror::Error)]
37pub enum DaemonError {
38 #[error("daemon already running: {0}")]
39 AlreadyRunning(String),
40 #[error("failed to start daemon: {0}")]
41 StartError(String),
42 #[error("I/O error: {0}")]
43 Io(#[from] std::io::Error),
44 #[error("IPC error: {0}")]
45 Ipc(#[from] crate::ipc::IpcError),
46 #[error("Config error: {0}")]
47 Config(#[from] crate::config::ConfigError),
48 #[error("Wallpaper error: {0}")]
49 Wallpaper(#[from] crate::wallpaper::WallpaperError),
50}
51
52struct WaylandWindow {
58 display: *mut std::ffi::c_void,
59 surface: *mut std::ffi::c_void,
60}
61
62unsafe impl Send for WaylandWindow {}
69unsafe impl Sync for WaylandWindow {}
70
71impl HasWindowHandle for WaylandWindow {
72 fn window_handle(&self) -> Result<WindowHandle<'_>, raw_window_handle::HandleError> {
73 let surface = std::ptr::NonNull::new(self.surface)
74 .ok_or(raw_window_handle::HandleError::Unavailable)?;
75 let handle = WaylandWindowHandle::new(surface);
76 unsafe { Ok(WindowHandle::borrow_raw(RawWindowHandle::Wayland(handle))) }
77 }
78}
79
80impl HasDisplayHandle for WaylandWindow {
81 fn display_handle(&self) -> Result<DisplayHandle<'_>, raw_window_handle::HandleError> {
82 let display = std::ptr::NonNull::new(self.display)
83 .ok_or(raw_window_handle::HandleError::Unavailable)?;
84 let handle = WaylandDisplayHandle::new(display);
85 unsafe { Ok(DisplayHandle::borrow_raw(RawDisplayHandle::Wayland(handle))) }
86 }
87}
88
89#[derive(Clone)]
90struct OutputInfo {
91 name: String,
92 width: u32,
93 height: u32,
94 scale_factor: i32,
95 wl_output: wl_output::WlOutput,
96}
97
98#[derive(Clone)]
99struct OutputLifecycle {
100 name: String,
101 render_state: std::sync::Arc<tokio::sync::Mutex<RenderState>>,
102 active: std::sync::Arc<std::sync::atomic::AtomicBool>,
103}
104
105struct WaylandState {
106 registry_state: RegistryState,
107 output_state: OutputState,
108 compositor_state: CompositorState,
109 shm: Shm,
110 outputs: std::collections::HashMap<u32, OutputInfo>,
111 surfaces: Vec<(u32, LayerSurface)>,
112 viewporter: Option<WpViewporter>,
113 viewports: std::collections::HashMap<u32, WpViewport>,
114 output_lifecycles: std::collections::HashMap<u32, OutputLifecycle>,
115 pending_restores: std::collections::HashSet<u32>,
116 layer_shell: LayerShell,
118 compositor: wl_compositor::WlCompositor,
120 hotplug: Option<DaemonHotplug>,
123}
124
125struct SendDisplayPtr(*mut std::ffi::c_void);
134unsafe impl Send for SendDisplayPtr {}
135
136struct DaemonHotplug {
140 renderer: std::sync::Arc<std::sync::Mutex<Option<std::sync::Arc<Renderer>>>>,
141 config: crate::config::WallrConfig,
142 display_ptr: SendDisplayPtr,
143 render_states: std::sync::Arc<
148 tokio::sync::Mutex<
149 std::collections::HashMap<String, std::sync::Arc<tokio::sync::Mutex<RenderState>>>,
150 >,
151 >,
152}
153
154impl ProvidesRegistryState for WaylandState {
155 fn registry(&mut self) -> &mut RegistryState {
156 &mut self.registry_state
157 }
158
159 registry_handlers![OutputState,];
160}
161
162impl CompositorHandler for WaylandState {
163 fn scale_factor_changed(
164 &mut self,
165 _conn: &Connection,
166 _qh: &QueueHandle<Self>,
167 _surface: &wl_surface::WlSurface,
168 _new_factor: i32,
169 ) {
170 }
171 fn transform_changed(
172 &mut self,
173 _conn: &Connection,
174 _qh: &QueueHandle<Self>,
175 _surface: &wl_surface::WlSurface,
176 _new_transform: wl_output::Transform,
177 ) {
178 }
179 fn frame(
180 &mut self,
181 _conn: &Connection,
182 _qh: &QueueHandle<Self>,
183 _surface: &wl_surface::WlSurface,
184 _time: u32,
185 ) {
186 }
187 fn surface_enter(
188 &mut self,
189 _conn: &Connection,
190 _qh: &QueueHandle<Self>,
191 _surface: &wl_surface::WlSurface,
192 _output: &wl_output::WlOutput,
193 ) {
194 }
195 fn surface_leave(
196 &mut self,
197 _conn: &Connection,
198 _qh: &QueueHandle<Self>,
199 _surface: &wl_surface::WlSurface,
200 _output: &wl_output::WlOutput,
201 ) {
202 }
203}
204
205impl wayland_client::Dispatch<wayland_client::protocol::wl_region::WlRegion, ()> for WaylandState {
206 fn event(
207 _state: &mut WaylandState,
208 _region: &wayland_client::protocol::wl_region::WlRegion,
209 _event: wayland_client::protocol::wl_region::Event,
210 _data: &(),
211 _conn: &Connection,
212 _qh: &QueueHandle<WaylandState>,
213 ) {
214 }
215}
216
217impl Dispatch<WpViewporter, ()> for WaylandState {
218 fn event(
219 _state: &mut WaylandState,
220 _proxy: &WpViewporter,
221 _event: wp_viewporter::Event,
222 _data: &(),
223 _conn: &Connection,
224 _qh: &QueueHandle<WaylandState>,
225 ) {
226 }
227}
228
229impl Dispatch<WpViewport, ()> for WaylandState {
230 fn event(
231 _state: &mut WaylandState,
232 _proxy: &WpViewport,
233 _event: wp_viewport::Event,
234 _data: &(),
235 _conn: &Connection,
236 _qh: &QueueHandle<WaylandState>,
237 ) {
238 }
239}
240
241fn viewport_destination(configured: (u32, u32), physical: (u32, u32)) -> Option<(i32, i32)> {
242 let (width, height) = configured;
243 let (physical_width, physical_height) = physical;
244 match (width, height) {
245 (0, 0) => None,
246 (0, height) if physical_height > 0 => {
247 let width = (u64::from(height) * u64::from(physical_width)
248 + u64::from(physical_height) / 2)
249 / u64::from(physical_height);
250 Some((i32::try_from(width).ok()?, i32::try_from(height).ok()?))
251 }
252 (width, 0) if physical_width > 0 => {
253 let height = (u64::from(width) * u64::from(physical_height)
254 + u64::from(physical_width) / 2)
255 / u64::from(physical_width);
256 Some((i32::try_from(width).ok()?, i32::try_from(height).ok()?))
257 }
258 (width, height) => Some((i32::try_from(width).ok()?, i32::try_from(height).ok()?)),
259 }
260}
261
262#[cfg(test)]
263mod viewport_tests {
264 use super::{
265 VideoPresentAction, advance_gif_slots, clamp_max_fps, clamp_preload_frames,
266 is_transient_wallpaper_error, is_usable_wallpaper_file, persist_wallpaper_at,
267 read_wallpaper_state, validate_live_config, video_present_action, viewport_destination,
268 write_wallpaper_state,
269 };
270 use crate::renderer::FrameStatus;
271
272 #[test]
273 fn gif_slots_never_flip_to_an_unuploaded_frame() {
274 let advanced = advance_gif_slots(0, 0, 1, 1, true);
276 assert_eq!((advanced.0, advanced.1, advanced.3), (1, 1, true));
277
278 let retry = advance_gif_slots(0, 0, 2, 1, false);
280 assert_eq!((retry.0, retry.1, retry.3), (0, 0, false));
281
282 let retried = advance_gif_slots(0, 0, 2, 1, true);
284 assert_eq!((retried.0, retried.1, retried.3), (1, 1, true));
285
286 let same = advance_gif_slots(1, 1, 2, 1, true);
288 assert_eq!((same.0, same.1, same.3), (1, 1, true));
289
290 let jump = advance_gif_slots(1, 1, 2, 4, true);
293 assert_eq!((jump.0, jump.1, jump.3), (0, 4, true));
294 }
295
296 #[test]
297 fn preserves_complete_configure_size() {
298 assert_eq!(
299 viewport_destination((3072, 1728), (3840, 2160)),
300 Some((3072, 1728))
301 );
302 }
303
304 #[test]
305 fn derives_missing_dimension_from_physical_aspect_ratio() {
306 assert_eq!(
307 viewport_destination((0, 1728), (3840, 2160)),
308 Some((3072, 1728))
309 );
310 assert_eq!(
311 viewport_destination((3072, 0), (3840, 2160)),
312 Some((3072, 1728))
313 );
314 assert_eq!(viewport_destination((0, 0), (3840, 2160)), None);
315 }
316
317 #[test]
318 fn retries_recoverable_video_surface_failures() {
319 assert_eq!(
320 video_present_action(FrameStatus::TimedOut),
321 VideoPresentAction::Retry
322 );
323 assert_eq!(
324 video_present_action(FrameStatus::Outdated),
325 VideoPresentAction::Reconfigure
326 );
327 assert_eq!(
328 video_present_action(FrameStatus::Lost),
329 VideoPresentAction::Reconfigure
330 );
331 }
332
333 #[test]
334 fn rotates_wallpaper_state_only_after_successful_persistence() {
335 let temporary = tempfile::tempdir().expect("temporary directory");
336 let first = temporary.path().join("first.jpg");
337 let second = temporary.path().join("second.jpg");
338 std::fs::write(&first, b"first").expect("first wallpaper");
339 std::fs::write(&second, b"second").expect("second wallpaper");
340
341 persist_wallpaper_at(temporary.path(), "DP-1", &first).expect("persist first");
342 assert_eq!(
343 read_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1"),
344 Some(first.clone())
345 );
346 assert_eq!(
347 read_wallpaper_state(temporary.path(), "previous_wallpaper", "DP-1"),
348 None
349 );
350
351 persist_wallpaper_at(temporary.path(), "DP-1", &second).expect("persist second");
352 assert_eq!(
353 read_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1"),
354 Some(second)
355 );
356 assert_eq!(
357 read_wallpaper_state(temporary.path(), "previous_wallpaper", "DP-1"),
358 Some(first)
359 );
360
361 let missing = temporary.path().join("missing.jpg");
362 write_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1", &missing)
363 .expect("persist missing path for restore test");
364 assert_eq!(
365 read_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1"),
366 Some(missing)
367 );
368 }
369
370 #[test]
371 fn wallpaper_state_preserves_non_utf8_and_whitespace() {
372 use std::os::unix::ffi::OsStringExt;
373
374 let temporary = tempfile::tempdir().expect("temporary directory");
375 let wallpaper = std::path::PathBuf::from(std::ffi::OsString::from_vec(
376 b" /tmp/wallpaper-\xff.jpg ".to_vec(),
377 ));
378
379 write_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1", &wallpaper)
380 .expect("persist wallpaper path");
381
382 assert_eq!(
383 read_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1"),
384 Some(wallpaper)
385 );
386 }
387
388 #[test]
389 fn retries_only_explicitly_transient_errors() {
390 let timed_out = anyhow::Error::new(std::io::Error::new(
391 std::io::ErrorKind::TimedOut,
392 "temporary timeout",
393 ));
394 let missing = anyhow::Error::new(std::io::Error::new(
395 std::io::ErrorKind::NotFound,
396 "missing wallpaper",
397 ));
398 let recoverable_video = anyhow::Error::new(crate::video::VideoError::QueueFull);
399
400 assert!(is_transient_wallpaper_error(&timed_out));
401 assert!(is_transient_wallpaper_error(&recoverable_video));
402 assert!(!is_transient_wallpaper_error(&missing));
403 assert!(!is_transient_wallpaper_error(&anyhow::anyhow!(
404 "invalid dimensions"
405 )));
406 }
407
408 #[test]
409 fn live_video_params_stay_bounded() {
410 assert_eq!(clamp_preload_frames(0), 1);
411 assert_eq!(clamp_preload_frames(2), 2);
412 assert_eq!(clamp_preload_frames(100), 3);
413 assert_eq!(clamp_max_fps(None), None);
414 assert_eq!(clamp_max_fps(Some(0)), None);
415 assert_eq!(clamp_max_fps(Some(60)), Some(60));
416 assert_eq!(clamp_max_fps(Some(10_000)), Some(240));
417 }
418
419 #[test]
420 fn watcher_requires_non_empty_files() {
421 let temporary = tempfile::tempdir().expect("temporary directory");
422 let empty = temporary.path().join("empty.jpg");
423 let filled = temporary.path().join("filled.jpg");
424 std::fs::write(&empty, b"").expect("empty file");
425 std::fs::write(&filled, b"data").expect("filled file");
426 assert!(!is_usable_wallpaper_file(&empty));
427 assert!(is_usable_wallpaper_file(&filled));
428 assert!(!is_usable_wallpaper_file(
429 &temporary.path().join("missing.jpg")
430 ));
431 }
432
433 #[test]
434 fn reload_rejects_absurd_live_values() {
435 let mut cfg = crate::config::WallrConfig::default();
436 assert!(validate_live_config(&cfg).is_ok());
437 cfg.video.preload_frames = 1000;
438 assert!(validate_live_config(&cfg).is_err());
439 cfg.video.preload_frames = 2;
440 cfg.daemon.max_fps = Some(0);
441 assert!(validate_live_config(&cfg).is_err());
442 }
443}
444
445impl LayerShellHandler for WaylandState {
446 fn configure(
447 &mut self,
448 _conn: &Connection,
449 _qh: &QueueHandle<Self>,
450 layer: &LayerSurface,
451 configure: LayerSurfaceConfigure,
452 _serial: u32,
453 ) {
454 let output_id = self.surfaces.iter().find_map(|(output_id, surface)| {
457 (surface.wl_surface().id() == layer.wl_surface().id()).then_some(*output_id)
458 });
459 let Some(output_id) = output_id else {
460 return;
461 };
462 let destination = self.outputs.get(&output_id).and_then(|output| {
463 viewport_destination(configure.new_size, (output.width, output.height))
464 });
465 if let Some(viewport) = self.viewports.get(&output_id) {
466 let Some((logical_width, logical_height)) = destination else {
467 tracing::warn!(
468 "Output {output_id} configure omitted both dimensions; waiting for a usable size"
469 );
470 return;
471 };
472 viewport.set_destination(logical_width, logical_height);
473 tracing::debug!(
474 "Configured viewport destination for output {output_id}: {logical_width}x{logical_height}"
475 );
476 }
477 if !self.pending_restores.remove(&output_id) {
478 return;
479 }
480 let Some(lifecycle) = self.output_lifecycles.get(&output_id).cloned() else {
481 return;
482 };
483 let Some(render_states) = self
484 .hotplug
485 .as_ref()
486 .map(|hotplug| hotplug.render_states.clone())
487 else {
488 return;
489 };
490
491 tokio::spawn(async move {
492 if !lifecycle.active.load(Ordering::SeqCst) {
493 return;
494 }
495 restore_cached_wallpaper(&lifecycle.name, &lifecycle.render_state).await;
496
497 let mut states = render_states.lock().await;
498 if lifecycle.active.load(Ordering::SeqCst) {
499 states.insert(lifecycle.name.clone(), lifecycle.render_state);
500 tracing::info!("Output configured: {}", lifecycle.name);
501 }
502 });
503 }
504
505 fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {}
506}
507
508impl ShmHandler for WaylandState {
509 fn shm_state(&mut self) -> &mut Shm {
510 &mut self.shm
511 }
512}
513
514impl OutputHandler for WaylandState {
515 fn output_state(&mut self) -> &mut OutputState {
516 &mut self.output_state
517 }
518 fn new_output(
519 &mut self,
520 _conn: &Connection,
521 _qh: &QueueHandle<Self>,
522 output: wl_output::WlOutput,
523 ) {
524 let id = output.id().protocol_id();
525 tracing::info!("Output detected: protocol_id={id}");
526
527 let mut info = OutputInfo {
528 name: format!("output-{id}"),
529 width: 1920,
530 height: 1080,
531 scale_factor: 1,
532 wl_output: output,
533 };
534
535 if let Some(info_data) = self.output_state.info(&info.wl_output) {
537 if let Some(mode) = info_data.modes.iter().find(|m| m.current) {
538 info.width = mode.dimensions.0 as u32;
539 info.height = mode.dimensions.1 as u32;
540 }
541 info.scale_factor = info_data.scale_factor;
542 let resolved = info_data
543 .name
544 .as_deref()
545 .filter(|n| !n.is_empty())
546 .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
547 if let Some(real_name) = resolved {
548 info.name = real_name.to_string();
549 } else if !info_data.make.is_empty() || !info_data.model.is_empty() {
550 let fallback = format!("{} {}", info_data.make, info_data.model)
551 .trim()
552 .to_string();
553 if !fallback.is_empty() {
554 info.name = fallback;
555 }
556 }
557 }
558
559 self.outputs.insert(id, info.clone());
560
561 if self.hotplug.is_some() {
565 let name = info.name.clone();
566 let renderer = self.hotplug.as_ref().unwrap().renderer.clone();
567 let display_ptr = self.hotplug.as_ref().unwrap().display_ptr.0;
568 let config = self.hotplug.as_ref().unwrap().config.clone();
569 match create_render_state_for_output_sync(
570 &renderer,
571 display_ptr,
572 self,
573 _qh,
574 &info,
575 &config,
576 ) {
577 Ok(rs) => {
578 let rs = std::sync::Arc::new(tokio::sync::Mutex::new(rs));
579 self.output_lifecycles.insert(
580 id,
581 OutputLifecycle {
582 name,
583 render_state: rs,
584 active: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)),
585 },
586 );
587 self.pending_restores.insert(id);
588 }
589 Err(e) => {
590 tracing::error!("Hotplug: failed to create render state for {name}: {e}");
591 }
592 }
593 }
594 }
595 fn update_output(
596 &mut self,
597 _conn: &Connection,
598 _qh: &QueueHandle<Self>,
599 output: wl_output::WlOutput,
600 ) {
601 let id = output.id().protocol_id();
602 if let Some(info) = self.outputs.get_mut(&id) {
603 let mut new_width = info.width;
604 let mut new_height = info.height;
605 let mut new_scale = info.scale_factor;
606
607 if let Some(mode) = self
608 .output_state
609 .info(&output)
610 .and_then(|i| i.modes.iter().find(|m| m.current).cloned())
611 {
612 new_width = mode.dimensions.0 as u32;
613 new_height = mode.dimensions.1 as u32;
614 }
615 if let Some(info_data) = self.output_state.info(&output) {
616 new_scale = info_data.scale_factor;
617 let resolved = info_data
618 .name
619 .as_deref()
620 .filter(|n| !n.is_empty())
621 .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
622 if let Some(real_name) = resolved {
623 if real_name != info.name {
624 tracing::info!(
625 "Output {id}: resolved name '{}' -> '{}'",
626 info.name,
627 real_name
628 );
629 info.name = real_name.to_string();
630 }
631 } else if info.name.starts_with("output-")
632 && (!info_data.make.is_empty() || !info_data.model.is_empty())
633 {
634 let fallback = format!("{} {}", info_data.make, info_data.model)
635 .trim()
636 .to_string();
637 if !fallback.is_empty() {
638 tracing::info!(
639 "Output {id}: fallback name '{}' -> '{}'",
640 info.name,
641 fallback
642 );
643 info.name = fallback;
644 }
645 }
646 }
647
648 let changed = new_width != info.width
649 || new_height != info.height
650 || new_scale != info.scale_factor;
651
652 info.width = new_width;
653 info.height = new_height;
654 info.scale_factor = new_scale;
655
656 if changed {
658 let name = info.name.clone();
659 if let Some(ref hotplug) = self.hotplug {
660 let render_states = hotplug.render_states.clone();
661 tokio::spawn(async move {
662 let states = render_states.lock().await;
663 if let Some(rs) = states.get(&name) {
664 let mut lock = rs.lock().await;
665 lock.width = new_width;
666 lock.height = new_height;
667 if let Some(gpu) = lock.gpu.as_ref() {
668 if let Ok(shared) = lock.renderer.lock()
669 && let Some(renderer) = shared.as_ref()
670 {
671 gpu.surface.configure(
672 &renderer.device,
673 &wgpu::SurfaceConfiguration {
674 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
675 format: gpu.format,
676 width: new_width,
677 height: new_height,
678 present_mode: wgpu::PresentMode::Fifo,
679 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
680 view_formats: vec![],
681 color_space: wgpu::SurfaceColorSpace::default(),
682 desired_maximum_frame_latency: 1,
683 },
684 );
685 }
686 tracing::info!(
687 "Hotplug: reconfigured {name} to {new_width}x{new_height}"
688 );
689 }
690 }
691 });
692 }
693 }
694 }
695 }
696 fn output_destroyed(
697 &mut self,
698 _conn: &Connection,
699 _qh: &QueueHandle<Self>,
700 output: wl_output::WlOutput,
701 ) {
702 let id = output.id().protocol_id();
703 if let Some(info) = self.outputs.remove(&id) {
704 tracing::info!("Output disconnected: {} (protocol_id={})", info.name, id);
705 self.pending_restores.remove(&id);
706 let viewport = self.viewports.remove(&id);
707 let layer_surface = self
708 .surfaces
709 .iter()
710 .position(|(pid, _)| *pid == id)
711 .map(|position| self.surfaces.swap_remove(position).1);
712 let lifecycle = self.output_lifecycles.remove(&id);
713 if let Some(lifecycle) = &lifecycle {
714 lifecycle.active.store(false, Ordering::SeqCst);
715 }
716 if let (Some(hotplug), Some(lifecycle)) = (&self.hotplug, lifecycle) {
718 let render_states = hotplug.render_states.clone();
719 tokio::spawn(async move {
720 let mut states = render_states.lock().await;
721 if states
722 .get(&lifecycle.name)
723 .is_some_and(|state| Arc::ptr_eq(state, &lifecycle.render_state))
724 {
725 states.remove(&lifecycle.name);
726 }
727 drop(states);
728
729 let state = lifecycle.render_state.lock().await;
730 state.playback_gen.fetch_add(1, Ordering::SeqCst);
731 state.pacer.notify();
732 state.video_playback.stop();
733 let render_lock = state.render_lock.clone();
734 drop(state);
735
736 let _ = tokio::task::spawn_blocking(move || {
737 drop(
738 render_lock
739 .lock()
740 .unwrap_or_else(|poisoned| poisoned.into_inner()),
741 );
742 })
743 .await;
744 if let Some(viewport) = viewport {
745 viewport.destroy();
746 }
747 drop(layer_surface);
748 tracing::info!("Hotplug: cleaned up render state for {}", lifecycle.name);
749 });
750 } else {
751 if let Some(viewport) = viewport {
752 viewport.destroy();
753 }
754 drop(layer_surface);
755 }
756 }
757 }
758}
759
760delegate_registry!(WaylandState);
761
762smithay_client_toolkit::delegate_dispatch2!(WaylandState);
763
764struct LivePacer {
766 lock: std::sync::Mutex<()>,
767 cond: std::sync::Condvar,
768}
769
770impl LivePacer {
771 fn new() -> Self {
772 Self {
773 lock: std::sync::Mutex::new(()),
774 cond: std::sync::Condvar::new(),
775 }
776 }
777
778 fn notify(&self) {
779 let _guard = self.lock.lock().unwrap();
780 self.cond.notify_all();
781 }
782
783 fn wait_until(&self, deadline: std::time::Instant) {
786 let guard = self.lock.lock().unwrap();
787 let now = std::time::Instant::now();
788 if deadline <= now {
789 return;
790 }
791 let _ = self.cond.wait_timeout(guard, deadline - now);
799 }
800}
801
802struct RenderState {
803 gpu: Option<GpuState>,
807 renderer: std::sync::Arc<std::sync::Mutex<Option<std::sync::Arc<Renderer>>>>,
808 display_ptr: SendDisplayPtr,
809 raw_surface: SendDisplayPtr,
810 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
814 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
817 pacer: std::sync::Arc<LivePacer>,
820 current_bind: Option<wgpu::BindGroup>,
821 current_tex: Option<wgpu::Texture>,
822 width: u32,
823 height: u32,
824 current_width: u32,
825 current_height: u32,
826 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
828 hw_accel: crate::video::HwAccel,
830 preferred_gpu: crate::video::GpuSelection,
832 preload_frames: usize,
834 max_fps: Option<u32>,
836 scaling_mode: u32,
838 current_effect: Option<crate::animation::Effect>,
840 last_wallpaper: Option<std::path::PathBuf>,
843 pre_blank: Option<(std::path::PathBuf, u32)>,
845 blanked: bool,
847 gif_paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
849 shm_surface: wl_surface::WlSurface,
853 shm_server: wayland_client::protocol::wl_shm::WlShm,
856 shm_pool: SlotPool,
857 shm_buffers: [Option<ShmBuffer>; 2],
862 shm_index: usize,
863 shm_width: u32,
864 shm_height: u32,
865 cached_shm_image: Option<CachedShmImage>,
869 gpu_surface_used: bool,
874 cached_video_texture: Option<std::sync::Arc<crate::renderer::VideoTexture>>,
879 loop_video: bool,
881}
882
883struct CachedShmImage {
884 path: std::path::PathBuf,
885 modified: std::time::SystemTime,
886 size: u64,
887 scaling_mode: u32,
888 output_width: u32,
889 output_height: u32,
890 width: u32,
891 height: u32,
892 compressed: Vec<u8>,
893 raw_len: usize,
894}
895
896struct GpuState {
897 renderer: std::sync::Arc<Renderer>,
898 surface: std::sync::Arc<wgpu::Surface<'static>>,
901 format: wgpu::TextureFormat,
902 per_output_uniforms: std::sync::Arc<crate::renderer::PerOutputUniforms>,
903}
904
905struct CommitData {
908 bg_bind: wgpu::BindGroup,
909 new_bind: wgpu::BindGroup,
910 img_width: u32,
911 img_height: u32,
912 old_img_width: u32,
913 old_img_height: u32,
914 format: wgpu::TextureFormat,
915 width: u32,
916 height: u32,
917 animated: Option<crate::animated::AnimatedImage>,
920 is_video: bool,
922 video_texture: Option<std::sync::Arc<crate::renderer::VideoTexture>>,
925 generation: u64,
928 scaling_mode: u32,
930 max_fps: Option<u32>,
932}
933
934impl RenderState {
935 fn ensure_gpu(&mut self) -> anyhow::Result<()> {
936 if self.gpu.is_some() {
937 return Ok(());
938 }
939
940 let renderer = {
941 let mut shared = self
942 .renderer
943 .lock()
944 .map_err(|_| anyhow::anyhow!("renderer initialization lock poisoned"))?;
945 if let Some(renderer) = shared.as_ref() {
946 renderer.clone()
947 } else {
948 let renderer = tokio::runtime::Handle::current()
949 .block_on(Renderer::new(&self.preferred_gpu))
950 .map_err(|e| anyhow::anyhow!("GPU init failed: {e}"))?;
951 let renderer = std::sync::Arc::new(renderer);
952 *shared = Some(renderer.clone());
953 renderer
954 }
955 };
956
957 let window_handle = WaylandWindow {
958 display: self.display_ptr.0,
959 surface: self.raw_surface.0,
960 };
961 let wgpu_surface = renderer
962 .instance
963 .create_surface(&window_handle)
964 .map_err(|e| anyhow::anyhow!("wgpu surface creation failed: {e:?}"))?;
965 let formats = wgpu_surface.get_capabilities(&renderer.adapter).formats;
966 let format = formats
967 .iter()
968 .copied()
969 .find(|format| *format == wgpu::TextureFormat::Bgra8UnormSrgb)
970 .or_else(|| formats.into_iter().next())
971 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb);
972 wgpu_surface.configure(
973 &renderer.device,
974 &wgpu::SurfaceConfiguration {
975 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
976 format,
977 width: self.width,
978 height: self.height,
979 present_mode: wgpu::PresentMode::Fifo,
980 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
981 view_formats: vec![],
982 color_space: wgpu::SurfaceColorSpace::default(),
983 desired_maximum_frame_latency: 1,
984 },
985 );
986 let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
1000 let surface = std::sync::Arc::new(wgpu_surface);
1001 self.gpu = Some(GpuState {
1002 renderer: renderer.clone(),
1003 surface,
1004 format,
1005 per_output_uniforms: std::sync::Arc::new(renderer.create_per_output_uniforms()),
1006 });
1007 Ok(())
1008 }
1009
1010 fn set_wallpaper(
1011 &mut self,
1012 path: &std::path::Path,
1013 effect: &crate::animation::Effect,
1014 duration_ms: u32,
1015 scaling_mode: u32,
1016 ) -> anyhow::Result<()> {
1017 if !self.blanked
1023 && (self.current_bind.is_some() || self.shm_buffers.iter().any(|b| b.is_some()))
1024 && self.last_wallpaper.as_deref() == Some(path)
1025 && self.scaling_mode == scaling_mode
1026 && self.current_effect.as_ref() == Some(effect)
1027 {
1028 return Ok(());
1029 }
1030 let is_animated_image = matches!(
1035 path.extension().and_then(|extension| extension.to_str()),
1036 Some("gif" | "GIF" | "apng" | "APNG")
1037 );
1038 if duration_ms == 0
1039 && !is_animated_image
1040 && !crate::video::VideoDecoder::is_video_file(path)
1041 && !self.gpu_surface_used
1042 {
1043 self.playback_gen.fetch_add(1, Ordering::SeqCst);
1048 self.pacer.notify();
1049 self.video_playback.stop();
1050 }
1051 if duration_ms == 0
1052 && !is_animated_image
1053 && !crate::video::VideoDecoder::is_video_file(path)
1054 && !self.gpu_surface_used
1055 && self.commit_static_shm(path, scaling_mode)?
1056 {
1057 self.current_bind = None;
1058 self.current_tex = None;
1059 self.current_width = 0;
1060 self.current_height = 0;
1061 self.scaling_mode = scaling_mode;
1062 self.current_effect = Some(effect.clone());
1063 self.last_wallpaper = Some(path.to_path_buf());
1064 self.blanked = false;
1065 return Ok(());
1071 }
1072 self.ensure_gpu()?;
1073 let commit = self.commit_wallpaper(path, scaling_mode)?;
1074 self.gpu_surface_used = true;
1075 self.scaling_mode = scaling_mode;
1076 self.current_effect = Some(effect.clone());
1077 self.spawn_transition(commit, effect, duration_ms);
1078 self.last_wallpaper = Some(path.to_path_buf());
1080 Ok(())
1081 }
1082
1083 fn commit_static_shm(
1084 &mut self,
1085 path: &std::path::Path,
1086 scaling_mode: u32,
1087 ) -> anyhow::Result<bool> {
1088 use image::{ImageDecoder, ImageReader};
1089
1090 let Some(_render_guard) = self.render_lock.try_lock().ok() else {
1099 return Ok(false);
1100 };
1101
1102 let source = std::fs::metadata(path)?;
1103 let modified = source.modified().unwrap_or(std::time::UNIX_EPOCH);
1104 let cached = self.cached_shm_image.as_ref().filter(|cached| {
1105 cached.path == path
1106 && cached.modified == modified
1107 && cached.size == source.len()
1108 && cached.scaling_mode == scaling_mode
1109 && cached.output_width == self.width
1110 && cached.output_height == self.height
1111 });
1112 let cached_rgba = cached.and_then(|cached| {
1113 zstd::bulk::decompress(&cached.compressed, cached.raw_len)
1114 .ok()
1115 .map(|rgba| (rgba, cached.width, cached.height))
1116 });
1117 let (rgba, width, height) = if let Some((rgba, width, height)) = cached_rgba {
1118 (rgba, width, height)
1119 } else {
1120 let decoder = ImageReader::open(path)?.into_decoder()?;
1121 let (source_width, source_height) = decoder.dimensions();
1122 Renderer::validate_static_decode(source_width, source_height, decoder.total_bytes())?;
1123 let image = image::DynamicImage::from_decoder(decoder)?;
1124 let (rgba, width, height) = crate::renderer::prepare_image_rgba(
1125 &image,
1126 self.width,
1127 self.height,
1128 scaling_mode,
1129 wgpu::Limits::default().max_texture_dimension_2d,
1130 )?;
1131 let rgba = rgba.into_raw();
1132 let compressed = zstd::bulk::compress(&rgba, 1)?;
1133 if compressed.len() <= STATIC_CACHE_LIMIT_BYTES {
1134 self.cached_shm_image = Some(CachedShmImage {
1135 path: path.to_path_buf(),
1136 modified,
1137 size: source.len(),
1138 scaling_mode,
1139 output_width: self.width,
1140 output_height: self.height,
1141 width,
1142 height,
1143 raw_len: rgba.len(),
1144 compressed,
1145 });
1146 } else {
1147 self.cached_shm_image = None;
1148 }
1149 (rgba, width, height)
1150 };
1151 if width != self.width || height != self.height {
1152 return Ok(false);
1156 }
1157
1158 let stride = width
1159 .checked_mul(4)
1160 .ok_or_else(|| anyhow::anyhow!("static image stride overflow"))?;
1161 let mut used_slot = 0usize;
1167 let mut chosen: Option<ShmBuffer> = None;
1168 if self.shm_width == width && self.shm_height == height {
1169 for _ in 0..2 {
1170 let slot = self.shm_index % 2;
1171 self.shm_index = self.shm_index.wrapping_add(1);
1172 if let Some(buffer) = self.shm_buffers[slot].take() {
1173 if buffer.canvas(&mut self.shm_pool).is_some() {
1174 chosen = Some(buffer);
1175 used_slot = slot;
1176 break;
1177 }
1178 self.shm_buffers[slot] = Some(buffer);
1179 }
1180 }
1181 if chosen.is_none() {
1182 'wait: for _ in 0..10 {
1185 std::thread::sleep(std::time::Duration::from_millis(5));
1186 for slot in 0..2 {
1187 if let Some(buffer) = self.shm_buffers[slot].take() {
1188 if buffer.canvas(&mut self.shm_pool).is_some() {
1189 chosen = Some(buffer);
1190 used_slot = slot;
1191 break 'wait;
1192 }
1193 self.shm_buffers[slot] = Some(buffer);
1194 }
1195 }
1196 }
1197 }
1198 } else {
1199 self.shm_buffers = [None, None];
1201 }
1202 let buffer = match chosen {
1203 Some(buffer) => buffer,
1204 None => {
1205 if self.shm_width != width || self.shm_height != height {
1210 self.shm_buffers = [None, None];
1212 let (new_buffer, _) = self.shm_pool.create_buffer(
1213 width as i32,
1214 height as i32,
1215 stride as i32,
1216 wayland_client::protocol::wl_shm::Format::Xrgb8888,
1217 )?;
1218 used_slot = 0;
1219 new_buffer
1220 } else if let Some(slot) = self.shm_buffers.iter().position(|slot| slot.is_none()) {
1221 let (new_buffer, _) = self.shm_pool.create_buffer(
1223 width as i32,
1224 height as i32,
1225 stride as i32,
1226 wayland_client::protocol::wl_shm::Format::Xrgb8888,
1227 )?;
1228 used_slot = slot;
1229 new_buffer
1230 } else {
1231 return Ok(false);
1236 }
1237 }
1238 };
1239 let canvas = buffer
1240 .canvas(&mut self.shm_pool)
1241 .ok_or_else(|| anyhow::anyhow!("shared-memory wallpaper buffer is still active"))?;
1242 for (src, dst) in rgba
1243 .as_chunks::<4>()
1244 .0
1245 .iter()
1246 .zip(canvas.as_chunks_mut::<4>().0.iter_mut())
1247 {
1248 dst[0] = src[2];
1252 dst[1] = src[1];
1253 dst[2] = src[0];
1254 dst[3] = 0xff;
1255 }
1256 buffer
1257 .attach_to(&self.shm_surface)
1258 .map_err(|e| anyhow::anyhow!(e))?;
1259 self.shm_surface
1260 .damage_buffer(0, 0, width as i32, height as i32);
1261 self.shm_surface.commit();
1262 self.shm_buffers[used_slot] = Some(buffer);
1263 self.shm_width = width;
1264 self.shm_height = height;
1265 Ok(true)
1266 }
1267
1268 fn commit_wallpaper(
1272 &mut self,
1273 path: &std::path::Path,
1274 scaling_mode: u32,
1275 ) -> anyhow::Result<CommitData> {
1276 use image::{ImageDecoder, ImageReader};
1277 let gpu = self
1278 .gpu
1279 .as_ref()
1280 .expect("GPU state required for dynamic content");
1281
1282 if crate::video::VideoDecoder::is_video_file(path) {
1284 tracing::info!("Video file detected: {:?}", path);
1285
1286 let generation = self.playback_gen.load(Ordering::SeqCst).wrapping_add(1);
1287 let renderer = gpu.renderer.clone();
1288 let mut prepared = crate::video::VideoPlayback::prepare(
1292 path,
1293 self.hw_accel,
1294 self.preload_frames,
1295 self.loop_video,
1296 std::time::Duration::from_secs(5),
1299 move |metadata| {
1300 renderer
1301 .validate_video_texture(metadata.width, metadata.height)
1302 .map_err(crate::video::VideoError::GpuResourceCreation)
1303 },
1304 )?;
1305 prepared.set_max_output_fps(self.max_fps);
1306 let metadata = prepared.metadata().clone();
1307 let first_frame = prepared.take_first_frame();
1308
1309 let (tex_width, tex_height) = first_frame
1310 .as_ref()
1311 .map(|frame| (frame.width, frame.height))
1312 .unwrap_or((metadata.width, metadata.height));
1313
1314 let video_texture = if let Some(cached) = self.cached_video_texture.as_ref()
1317 && cached.matches_dimensions(tex_width, tex_height)
1318 {
1319 tracing::debug!("Reusing video texture for {}x{}", tex_width, tex_height);
1320 cached.clone()
1322 } else {
1323 if let Some(previous) = self.cached_video_texture.as_ref() {
1324 tracing::debug!(
1325 "Video resolution changed, recreating texture: {}x{} -> {}x{}",
1326 previous.width(),
1327 previous.height(),
1328 tex_width,
1329 tex_height
1330 );
1331 }
1332 let new_texture =
1333 std::sync::Arc::new(gpu.renderer.create_video_texture(tex_width, tex_height)?);
1334 self.cached_video_texture = Some(new_texture.clone());
1336 new_texture
1337 };
1338 let (img_width, img_height) = if let Some(frame) = first_frame {
1339 gpu.renderer
1340 .update_video_texture(&video_texture, &frame.data)?;
1341 (frame.width, frame.height)
1342 } else {
1343 tracing::warn!("No first frame available, using black texture");
1345 (metadata.width, metadata.height)
1346 };
1347 self.video_playback.commit(prepared, generation);
1348 self.playback_gen.store(generation, Ordering::SeqCst);
1349 self.pacer.notify();
1350 let new_tex = video_texture.texture().clone();
1351 let new_bind = video_texture.bind_group().clone();
1352
1353 let old_bind = self.current_bind.take();
1354 let (old_img_width, old_img_height) = if old_bind.is_some() {
1355 (self.current_width.max(1), self.current_height.max(1))
1356 } else {
1357 (img_width, img_height)
1358 };
1359 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
1360
1361 drop(self.current_tex.take());
1362 self.current_tex = Some(new_tex);
1363 self.current_bind = Some(new_bind.clone());
1364 self.current_width = img_width;
1365 self.current_height = img_height;
1366
1367 return Ok(CommitData {
1368 bg_bind,
1369 new_bind,
1370 img_width,
1371 img_height,
1372 old_img_width,
1373 old_img_height,
1374 format: gpu.format,
1375 width: self.width,
1376 height: self.height,
1377 animated: None,
1378 is_video: true,
1379 video_texture: Some(video_texture),
1380 generation,
1381 scaling_mode,
1382 max_fps: self.max_fps,
1383 });
1384 }
1385
1386 let mut animated = crate::animated::AnimatedImage::decode(path)?;
1389 let (new_tex, new_bind, img_width, img_height) = if let Some(anim) = animated.as_mut() {
1390 let (w, h) = (anim.width, anim.height);
1391 let (tex, bind) = gpu.renderer.create_texture(w, h)?;
1392 let first = anim.first_frame();
1393 if !first.is_empty() {
1394 gpu.renderer.update_texture(&tex, first, w, h);
1395 }
1396 (tex, bind, w, h)
1397 } else {
1398 let decoder = ImageReader::open(path)?.into_decoder()?;
1399 let (source_width, source_height) = decoder.dimensions();
1400 Renderer::validate_static_decode(source_width, source_height, decoder.total_bytes())?;
1401 let new_img = image::DynamicImage::from_decoder(decoder)?;
1402 let (tex, bind, width, height) =
1403 gpu.renderer
1404 .load_texture(&new_img, self.width, self.height, scaling_mode)?;
1405 (tex, bind, width, height)
1406 };
1407
1408 self.video_playback.stop();
1411 self.cached_video_texture = None;
1414
1415 let old_bind = self.current_bind.take();
1416 let (old_img_width, old_img_height) = if old_bind.is_some() {
1417 (self.current_width.max(1), self.current_height.max(1))
1418 } else {
1419 (img_width, img_height)
1420 };
1421 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
1426
1427 drop(self.current_tex.take());
1428 self.current_tex = Some(new_tex);
1429 self.current_bind = Some(new_bind.clone());
1430 self.current_width = img_width;
1431 self.current_height = img_height;
1432
1433 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
1434 self.pacer.notify();
1435
1436 Ok(CommitData {
1437 bg_bind,
1438 new_bind,
1439 img_width,
1440 img_height,
1441 old_img_width,
1442 old_img_height,
1443 format: gpu.format,
1444 width: self.width,
1445 height: self.height,
1446 animated,
1447 is_video: false,
1448 video_texture: None,
1449 generation,
1450 scaling_mode,
1451 max_fps: self.max_fps,
1452 })
1453 }
1454
1455 fn spawn_transition(
1460 &self,
1461 commit: CommitData,
1462 effect: &crate::animation::Effect,
1463 duration_ms: u32,
1464 ) {
1465 let gpu = self
1466 .gpu
1467 .as_ref()
1468 .expect("GPU state required for transitions");
1469 let renderer = gpu.renderer.clone();
1470 let surface = std::sync::Arc::clone(&gpu.surface);
1471 let render_lock = self.render_lock.clone();
1472 let playback_gen = self.playback_gen.clone();
1473 let pacer = self.pacer.clone();
1474 let video_playback = self.video_playback.clone();
1475 let per_output_uniforms = std::sync::Arc::clone(&gpu.per_output_uniforms);
1476 let gif_paused = self.gif_paused.clone();
1477 let effect = effect.clone();
1478 drop(tokio::task::spawn_blocking(move || {
1479 render_transition(
1480 renderer,
1481 surface,
1482 render_lock,
1483 playback_gen,
1484 pacer,
1485 video_playback,
1486 gif_paused,
1487 commit,
1488 effect,
1489 duration_ms,
1490 &per_output_uniforms,
1491 );
1492 }));
1493 }
1494}
1495
1496async fn restore_cached_wallpaper(name: &str, render_state: &Arc<Mutex<RenderState>>) {
1497 let state_root = wallpaper_state_root();
1498 let Some(path) = read_wallpaper_state(&state_root, "last_wallpaper", name) else {
1499 return;
1500 };
1501
1502 let effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1503 if let Err(err) = set_wallpaper_with_retry(render_state, &path, &effect, 0, 0).await {
1508 tracing::warn!("Failed to restore wallpaper for {name} from {path:?}: {err}");
1509 let Some(previous) = read_wallpaper_state(&state_root, "previous_wallpaper", name) else {
1510 return;
1511 };
1512 match set_wallpaper_with_retry(render_state, &previous, &effect, 0, 0).await {
1513 Ok(()) => {
1514 if let Err(persist_err) =
1515 write_wallpaper_state(&state_root, "last_wallpaper", name, &previous)
1516 {
1517 tracing::warn!(
1518 "Restored previous wallpaper for {name}, but failed to update state: {persist_err}"
1519 );
1520 } else {
1521 tracing::warn!("Restored previous wallpaper for {name} after {path:?} failed");
1522 }
1523 }
1524 Err(previous_err) => tracing::warn!(
1525 "Failed to restore previous wallpaper for {name} from {previous:?}: {previous_err}"
1526 ),
1527 }
1528 }
1529}
1530
1531const WALLPAPER_RETRY_ATTEMPTS: usize = 3;
1532const WALLPAPER_RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(150);
1533const INITIAL_SHM_POOL_BYTES: usize = 4096;
1537const STATIC_CACHE_LIMIT_BYTES: usize = 4 * 1024 * 1024;
1541
1542async fn set_wallpaper_with_retry(
1543 render_state: &Arc<Mutex<RenderState>>,
1544 path: &std::path::Path,
1545 effect: &crate::animation::Effect,
1546 duration_ms: u32,
1547 scaling_mode: u32,
1548) -> anyhow::Result<()> {
1549 let render_state = Arc::clone(render_state);
1550 let path = path.to_path_buf();
1551 let effect = effect.clone();
1552
1553 tokio::task::spawn_blocking(move || {
1554 let mut attempt = 1;
1555 loop {
1556 let result = {
1557 let mut state = render_state.blocking_lock();
1558 state.set_wallpaper(&path, &effect, duration_ms, scaling_mode)
1559 };
1560 match result {
1561 Ok(()) => return Ok(()),
1562 Err(err)
1563 if attempt < WALLPAPER_RETRY_ATTEMPTS
1564 && is_transient_wallpaper_error(&err) =>
1565 {
1566 tracing::warn!(
1567 "Transient wallpaper error for {path:?} (attempt {attempt}/{WALLPAPER_RETRY_ATTEMPTS}): {err}"
1568 );
1569 attempt += 1;
1570 std::thread::sleep(WALLPAPER_RETRY_DELAY);
1571 }
1572 Err(err) => return Err(err),
1573 }
1574 }
1575 })
1576 .await?
1577}
1578
1579fn is_transient_wallpaper_error(err: &anyhow::Error) -> bool {
1580 err.chain().any(|cause| {
1581 cause
1582 .downcast_ref::<std::io::Error>()
1583 .is_some_and(|io_err| {
1584 matches!(
1585 io_err.kind(),
1586 std::io::ErrorKind::Interrupted
1587 | std::io::ErrorKind::WouldBlock
1588 | std::io::ErrorKind::TimedOut
1589 )
1590 })
1591 || cause
1592 .downcast_ref::<crate::video::VideoError>()
1593 .is_some_and(crate::video::VideoError::is_recoverable)
1594 })
1595}
1596
1597fn wallpaper_state_root() -> std::path::PathBuf {
1598 dirs::cache_dir()
1599 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1600 .join("wallr")
1601}
1602
1603fn wallpaper_state_path(root: &std::path::Path, state_dir: &str, name: &str) -> std::path::PathBuf {
1604 root.join(state_dir).join(name)
1605}
1606
1607fn read_wallpaper_state(
1608 root: &std::path::Path,
1609 state_dir: &str,
1610 name: &str,
1611) -> Option<std::path::PathBuf> {
1612 use std::os::unix::ffi::OsStringExt;
1613
1614 let path = std::fs::read(wallpaper_state_path(root, state_dir, name)).ok()?;
1615 let wallpaper = std::path::PathBuf::from(std::ffi::OsString::from_vec(path));
1616 (!wallpaper.as_os_str().is_empty()).then_some(wallpaper)
1617}
1618
1619fn write_wallpaper_state(
1620 root: &std::path::Path,
1621 state_dir: &str,
1622 name: &str,
1623 wallpaper: &std::path::Path,
1624) -> std::io::Result<()> {
1625 use std::io::Write;
1626 use std::os::unix::ffi::OsStrExt;
1627 use std::sync::atomic::{AtomicU64, Ordering};
1628
1629 static TEMP_ID: AtomicU64 = AtomicU64::new(0);
1630
1631 let state_path = wallpaper_state_path(root, state_dir, name);
1632 let parent = state_path
1633 .parent()
1634 .ok_or_else(|| std::io::Error::other("wallpaper state path has no parent"))?;
1635 std::fs::create_dir_all(parent)?;
1636 let file_name = state_path
1637 .file_name()
1638 .and_then(|value| value.to_str())
1639 .unwrap_or("wallpaper");
1640
1641 loop {
1642 let id = TEMP_ID.fetch_add(1, Ordering::Relaxed);
1643 let temporary_path = parent.join(format!(".{file_name}.{}.{}.tmp", std::process::id(), id));
1644 match std::fs::OpenOptions::new()
1645 .write(true)
1646 .create_new(true)
1647 .open(&temporary_path)
1648 {
1649 Ok(mut temporary) => {
1650 if let Err(err) = temporary
1651 .write_all(wallpaper.as_os_str().as_bytes())
1652 .and_then(|()| temporary.sync_all())
1653 .and_then(|()| std::fs::rename(&temporary_path, &state_path))
1654 {
1655 let _ = std::fs::remove_file(&temporary_path);
1656 return Err(err);
1657 }
1658 return Ok(());
1659 }
1660 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
1661 Err(err) => return Err(err),
1662 }
1663 }
1664}
1665
1666fn persist_wallpaper(name: &str, wallpaper: &std::path::Path) -> std::io::Result<()> {
1667 let root = wallpaper_state_root();
1668 persist_wallpaper_at(&root, name, wallpaper)
1669}
1670
1671fn persist_wallpaper_at(
1672 root: &std::path::Path,
1673 name: &str,
1674 wallpaper: &std::path::Path,
1675) -> std::io::Result<()> {
1676 let current = read_wallpaper_state(root, "last_wallpaper", name);
1677 if current.as_deref() == Some(wallpaper) {
1682 return Ok(());
1683 }
1684 let previous = current.filter(|current| current.exists() && current != wallpaper);
1685 write_wallpaper_state(root, "last_wallpaper", name, wallpaper)?;
1686 if let Some(previous) = previous {
1687 write_wallpaper_state(root, "previous_wallpaper", name, &previous)?;
1688 }
1689 Ok(())
1690}
1691
1692#[allow(clippy::too_many_arguments)]
1700fn render_transition(
1701 renderer: std::sync::Arc<Renderer>,
1702 surface: std::sync::Arc<wgpu::Surface<'static>>,
1703 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
1704 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
1705 pacer: std::sync::Arc<LivePacer>,
1706 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
1707 gif_paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
1708 mut commit: CommitData,
1709 effect: crate::animation::Effect,
1710 duration_ms: u32,
1711 per_output_uniforms: &crate::renderer::PerOutputUniforms,
1712) {
1713 if playback_gen.load(Ordering::Acquire) != commit.generation {
1717 reclaim_commit_resources(&renderer, commit);
1718 return;
1719 }
1720 let _guard = render_lock
1721 .lock()
1722 .unwrap_or_else(|poisoned| poisoned.into_inner());
1723
1724 let duration = std::time::Duration::from_millis(u64::from(duration_ms.max(1)));
1725 let start = std::time::Instant::now();
1726 loop {
1727 if playback_gen.load(Ordering::Acquire) != commit.generation {
1732 reclaim_commit_resources(&renderer, commit);
1733 return;
1734 }
1735 let progress = start.elapsed().as_secs_f32() / duration.as_secs_f32();
1736 let uniforms = crate::animation::compute_effect_uniforms(&effect, progress.clamp(0.0, 1.0));
1737 let status = renderer.render_frame(
1738 crate::renderer::FrameRequest {
1739 surface: &surface,
1740 format: commit.format,
1741 bg_bind: &commit.bg_bind,
1742 new_bind: &commit.new_bind,
1743 effect: &uniforms,
1744 width: commit.width,
1745 height: commit.height,
1746 img_width: commit.img_width,
1747 img_height: commit.img_height,
1748 old_img_width: commit.old_img_width,
1749 old_img_height: commit.old_img_height,
1750 scaling_mode: commit.scaling_mode,
1751 },
1752 per_output_uniforms,
1753 );
1754 let status = match status {
1755 Ok(status) => status,
1756 Err(err) => {
1757 eprintln!("wallr: transition render failed: {err}");
1758 break;
1759 }
1760 };
1761 if progress >= 1.0 {
1762 break;
1763 }
1764 match status {
1765 crate::renderer::FrameStatus::Presented => {}
1766 crate::renderer::FrameStatus::TimedOut => {
1771 std::thread::sleep(std::time::Duration::from_millis(4));
1772 }
1773 crate::renderer::FrameStatus::Outdated | crate::renderer::FrameStatus::Lost => {
1776 surface.configure(
1777 &renderer.device,
1778 &wgpu::SurfaceConfiguration {
1779 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1780 format: commit.format,
1781 width: commit.width.max(1),
1782 height: commit.height.max(1),
1783 present_mode: wgpu::PresentMode::Fifo,
1784 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
1785 view_formats: vec![],
1786 color_space: wgpu::SurfaceColorSpace::default(),
1787 desired_maximum_frame_latency: 1,
1788 },
1789 );
1790 }
1791 }
1792 }
1793
1794 let mut animated = commit.animated.take();
1798 if let Some(animated) = animated.as_mut()
1799 && playback_gen.load(Ordering::SeqCst) == commit.generation
1800 {
1801 play_live(
1802 &renderer,
1803 &surface,
1804 &commit,
1805 animated,
1806 &playback_gen,
1807 &pacer,
1808 &gif_paused,
1809 per_output_uniforms,
1810 );
1811 } else if commit.is_video && playback_gen.load(Ordering::SeqCst) == commit.generation {
1812 play_video(
1813 &renderer,
1814 &surface,
1815 &commit,
1816 &video_playback,
1817 &playback_gen,
1818 &pacer,
1819 per_output_uniforms,
1820 );
1821 }
1822 reclaim_commit_resources(&renderer, commit);
1826}
1827
1828fn reclaim_commit_resources(renderer: &Renderer, commit: CommitData) {
1829 drop(commit);
1830 renderer
1831 .device
1832 .poll(wgpu::PollType::wait_indefinitely())
1833 .ok();
1834}
1835
1836fn advance_gif_slots(
1844 cur: usize,
1845 cur_frame: usize,
1846 next_frame: usize,
1847 index: usize,
1848 upload_ok: bool,
1849) -> (usize, usize, usize, bool) {
1850 if index == cur_frame {
1851 return (cur, cur_frame, next_frame, true);
1852 }
1853 let target = cur ^ 1;
1854 if next_frame == index {
1855 return (target, index, next_frame, true);
1857 }
1858 if upload_ok {
1859 return (target, index, index, true);
1860 }
1861 (cur, cur_frame, next_frame, false)
1862}
1863
1864#[allow(clippy::too_many_arguments)]
1870fn play_live(
1871 renderer: &Renderer,
1872 surface: &wgpu::Surface<'static>,
1873 commit: &CommitData,
1874 animated: &mut crate::animated::AnimatedImage,
1875 playback_gen: &std::sync::atomic::AtomicU64,
1876 pacer: &LivePacer,
1877 gif_paused: &std::sync::Arc<std::sync::atomic::AtomicBool>,
1878 per_output_uniforms: &crate::renderer::PerOutputUniforms,
1879) {
1880 let Ok((tex_a, bind_a)) = renderer.create_texture(animated.width, animated.height) else {
1881 tracing::warn!(
1882 "GIF dimensions {}x{} exceed the GPU texture limit",
1883 animated.width,
1884 animated.height
1885 );
1886 return;
1887 };
1888 let Ok((tex_b, bind_b)) = renderer.create_texture(animated.width, animated.height) else {
1889 tracing::warn!(
1890 "GIF dimensions {}x{} exceed the GPU texture limit",
1891 animated.width,
1892 animated.height
1893 );
1894 return;
1895 };
1896 let (frame_w, frame_h) = (animated.width, animated.height);
1897 let (bytes_per_row, rows) = (frame_w * 4, frame_h);
1898 let frame_bytes = bytes_per_row as u64 * rows as u64;
1899
1900 let direct_upload = bytes_per_row % 256 == 0;
1903 let staging: Vec<wgpu::Buffer> = if direct_upload {
1904 (0..2)
1905 .map(|_| {
1906 renderer.device.create_buffer(&wgpu::BufferDescriptor {
1907 label: Some("wallr-gif-staging"),
1908 size: frame_bytes,
1909 usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
1910 mapped_at_creation: false,
1911 })
1912 })
1913 .collect()
1914 } else {
1915 Vec::new()
1916 };
1917
1918 let first = animated.first_frame();
1919 if !first.is_empty() {
1920 renderer.update_texture(&tex_a, first, frame_w, frame_h);
1921 renderer.update_texture(&tex_b, first, frame_w, frame_h);
1922 }
1923 let binds = [bind_a, bind_b];
1924 let textures = [tex_a, tex_b];
1925
1926 let upload = |renderer: &Renderer,
1929 tgt: usize,
1930 index: usize,
1931 slot: usize,
1932 animated: &mut crate::animated::AnimatedImage|
1933 -> bool {
1934 if direct_upload {
1935 let buffer = &staging[slot];
1936 let slice = buffer.slice(..);
1937 slice.map_async(wgpu::MapMode::Write, |_| {});
1938 renderer
1939 .device
1940 .poll(wgpu::PollType::wait_indefinitely())
1941 .ok();
1942 let size = slice.size() as usize;
1943 let mut tmp = vec![0u8; size];
1944 let ok = animated.decompress_into(index, &mut tmp);
1945 if ok {
1946 let mut mapped = slice.get_mapped_range_mut().unwrap();
1947 mapped.copy_from_slice(&tmp);
1948 }
1949 buffer.unmap();
1950 if ok {
1951 let mut encoder = renderer
1952 .device
1953 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
1954 encoder.copy_buffer_to_texture(
1955 wgpu::TexelCopyBufferInfo {
1956 buffer,
1957 layout: wgpu::TexelCopyBufferLayout {
1958 offset: 0,
1959 bytes_per_row: Some(bytes_per_row),
1960 rows_per_image: Some(rows),
1961 },
1962 },
1963 wgpu::TexelCopyTextureInfo {
1964 texture: &textures[tgt],
1965 mip_level: 0,
1966 origin: wgpu::Origin3d::ZERO,
1967 aspect: wgpu::TextureAspect::All,
1968 },
1969 wgpu::Extent3d {
1970 width: frame_w,
1971 height: frame_h,
1972 depth_or_array_layers: 1,
1973 },
1974 );
1975 renderer.queue.submit([encoder.finish()]);
1976 return true;
1977 }
1978 } else {
1979 let frame = animated.frame_at(index);
1980 if !frame.is_empty() {
1981 renderer.update_texture(&textures[tgt], frame, frame_w, frame_h);
1982 return true;
1983 }
1984 }
1985 false
1986 };
1987
1988 let mut cur = 0usize; let mut cur_frame = 0usize; let mut next_frame = 0usize; let mut slot = 0usize; let start = std::time::Instant::now();
1993 let mut paused_elapsed = std::time::Duration::ZERO; let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1995 loop {
1996 if playback_gen.load(Ordering::SeqCst) != commit.generation {
1997 return;
1998 }
1999
2000 if gif_paused.load(Ordering::SeqCst) {
2002 let pause_start = std::time::Instant::now();
2007 pacer.wait_until(std::time::Instant::now() + std::time::Duration::from_secs(86_400));
2008 paused_elapsed += pause_start.elapsed();
2009 continue;
2010 }
2011
2012 let index = animated.frame_index_at(start.elapsed() - paused_elapsed);
2013 if index != cur_frame {
2014 let target = cur ^ 1;
2023 let upload_ok = if next_frame == index {
2024 true
2025 } else {
2026 let ok = upload(renderer, target, index, slot, animated);
2027 if ok {
2028 slot ^= 1;
2029 }
2030 ok
2031 };
2032 let (new_cur, new_cur_frame, new_next, displayable) =
2033 advance_gif_slots(cur, cur_frame, next_frame, index, upload_ok);
2034 if !displayable {
2035 pacer.wait_until(std::time::Instant::now() + std::time::Duration::from_millis(2));
2040 continue;
2041 }
2042 cur = new_cur;
2043 cur_frame = new_cur_frame;
2044 next_frame = new_next;
2045 }
2046 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
2047 let status = renderer.render_frame(
2048 crate::renderer::FrameRequest {
2049 surface,
2050 format: commit.format,
2051 bg_bind: &binds[cur],
2052 new_bind: &binds[cur],
2053 effect: &uniforms,
2054 width: commit.width,
2055 height: commit.height,
2056 img_width: animated.width,
2057 img_height: animated.height,
2058 old_img_width: animated.width,
2059 old_img_height: animated.height,
2060 scaling_mode: commit.scaling_mode,
2061 },
2062 per_output_uniforms,
2063 );
2064 match status {
2065 Ok(crate::renderer::FrameStatus::Presented) => {}
2066 Ok(crate::renderer::FrameStatus::TimedOut) => {
2070 std::thread::sleep(std::time::Duration::from_millis(4));
2071 }
2072 Ok(crate::renderer::FrameStatus::Outdated | crate::renderer::FrameStatus::Lost) => {
2073 surface.configure(
2074 &renderer.device,
2075 &wgpu::SurfaceConfiguration {
2076 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2077 format: commit.format,
2078 width: commit.width.max(1),
2079 height: commit.height.max(1),
2080 present_mode: wgpu::PresentMode::Fifo,
2081 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
2082 view_formats: vec![],
2083 color_space: wgpu::SurfaceColorSpace::default(),
2084 desired_maximum_frame_latency: 1,
2085 },
2086 );
2087 }
2088 Err(err) => {
2089 tracing::warn!("GIF present failed ({err}), stopping playback");
2090 return;
2091 }
2092 }
2093
2094 let elapsed = start.elapsed() - paused_elapsed;
2102 let total: std::time::Duration = animated.total_duration();
2103 let loops = (elapsed.as_millis() / total.as_millis().max(1)) as u64;
2104 let next_change = animated.frame_start(index + 1) + total * (loops as u32);
2105 let wait = next_change.saturating_sub(elapsed);
2106 if wait > std::time::Duration::ZERO {
2107 let next = index + 1;
2108 if next_frame != next && upload(renderer, cur ^ 1, next, slot, animated) {
2109 slot ^= 1;
2110 next_frame = next;
2111 }
2112 pacer.wait_until(std::time::Instant::now() + wait);
2115 }
2116 }
2117}
2118
2119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2120enum VideoPresentAction {
2121 Presented,
2122 Retry,
2123 Reconfigure,
2124}
2125
2126fn video_present_action(status: crate::renderer::FrameStatus) -> VideoPresentAction {
2127 match status {
2128 crate::renderer::FrameStatus::Presented => VideoPresentAction::Presented,
2129 crate::renderer::FrameStatus::TimedOut => VideoPresentAction::Retry,
2130 crate::renderer::FrameStatus::Outdated | crate::renderer::FrameStatus::Lost => {
2131 VideoPresentAction::Reconfigure
2132 }
2133 }
2134}
2135
2136fn play_video(
2138 renderer: &Renderer,
2139 surface: &wgpu::Surface<'static>,
2140 commit: &CommitData,
2141 video_playback: &std::sync::Arc<crate::video::VideoPlayback>,
2142 playback_gen: &std::sync::atomic::AtomicU64,
2143 pacer: &LivePacer,
2144 per_output_uniforms: &crate::renderer::PerOutputUniforms,
2145) {
2146 let mut width = commit.img_width;
2147 let mut height = commit.img_height;
2148
2149 let Some(initial) = commit.video_texture.as_ref() else {
2150 tracing::warn!("Video conversion resources unavailable");
2151 return;
2152 };
2153 let mut owned_replacement: Option<crate::renderer::VideoTexture> = None;
2157 let mut texture: &crate::renderer::VideoTexture = initial.as_ref();
2158 let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
2159
2160 let min_frame_interval = commit
2161 .max_fps
2162 .filter(|fps| *fps > 0)
2163 .map(|fps| std::time::Duration::from_secs_f64(1.0 / f64::from(fps)));
2164 let mut last_present: Option<std::time::Instant> = None;
2165 let mut paused_last_iteration = false;
2166 loop {
2167 if playback_gen.load(Ordering::SeqCst) != commit.generation {
2172 return;
2173 }
2174
2175 if let (Some(interval), Some(previous)) = (min_frame_interval, last_present) {
2176 pacer.wait_until(previous + interval);
2177 if playback_gen.load(Ordering::SeqCst) != commit.generation {
2178 return;
2179 }
2180 }
2181
2182 let mut frame_uploaded = false;
2185 if let Some(frame) = video_playback.next_frame_in_generation(commit.generation) {
2186 if frame.width != width || frame.height != height {
2189 match renderer.create_video_texture(frame.width, frame.height) {
2190 Ok(replacement) => {
2191 tracing::info!(
2192 "Video resolution changed {}x{} -> {}x{}, recreated conversion resources",
2193 width,
2194 height,
2195 frame.width,
2196 frame.height
2197 );
2198 width = frame.width;
2199 height = frame.height;
2200 owned_replacement.take();
2204 owned_replacement = Some(replacement);
2205 texture = owned_replacement.as_ref().expect("just stored");
2206 }
2207 Err(err) => {
2208 tracing::warn!("Video resolution change rejected: {err}");
2209 pacer.wait_until(
2210 std::time::Instant::now() + std::time::Duration::from_millis(2),
2211 );
2212 continue;
2213 }
2214 }
2215 }
2216 if let Err(err) = renderer.update_video_texture(texture, &frame.data) {
2217 tracing::warn!("Video frame upload failed: {err}");
2220 } else {
2221 frame_uploaded = true;
2222 }
2223 }
2226
2227 if !frame_uploaded {
2228 if playback_gen.load(Ordering::SeqCst) != commit.generation {
2229 return;
2230 }
2231 if video_playback.is_paused() {
2232 last_present = None;
2237 paused_last_iteration = true;
2238 pacer.wait_until(std::time::Instant::now() + std::time::Duration::from_millis(100));
2239 continue;
2240 }
2241 if paused_last_iteration {
2242 last_present = None;
2246 paused_last_iteration = false;
2247 }
2248 if video_playback.decoder_state() == Some(crate::video::DecoderState::Failed) {
2249 tracing::warn!("video decoder failed, stopping playback");
2250 return;
2251 }
2252 let wait = video_playback
2253 .time_until_next_frame_in_generation(commit.generation)
2254 .unwrap_or(std::time::Duration::from_millis(2));
2255 if playback_gen.load(Ordering::SeqCst) != commit.generation {
2256 return;
2257 }
2258 pacer.wait_until(std::time::Instant::now() + wait);
2259 continue;
2260 }
2261
2262 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
2263 let status = renderer.render_frame(
2264 crate::renderer::FrameRequest {
2265 surface,
2266 format: commit.format,
2267 bg_bind: texture.bind_group(),
2268 new_bind: texture.bind_group(),
2269 effect: &uniforms,
2270 width: commit.width,
2271 height: commit.height,
2272 img_width: width,
2273 img_height: height,
2274 old_img_width: width,
2275 old_img_height: height,
2276 scaling_mode: commit.scaling_mode,
2277 },
2278 per_output_uniforms,
2279 );
2280
2281 match status.map(video_present_action) {
2282 Ok(VideoPresentAction::Presented) => {
2283 last_present = Some(std::time::Instant::now());
2284 }
2285 Ok(action) => {
2286 if action == VideoPresentAction::Reconfigure {
2287 surface.configure(
2288 &renderer.device,
2289 &wgpu::SurfaceConfiguration {
2290 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2291 format: commit.format,
2292 width: commit.width,
2293 height: commit.height,
2294 present_mode: wgpu::PresentMode::Fifo,
2295 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
2296 view_formats: vec![],
2297 color_space: wgpu::SurfaceColorSpace::default(),
2298 desired_maximum_frame_latency: 1,
2299 },
2300 );
2301 }
2302 pacer.wait_until(std::time::Instant::now() + std::time::Duration::from_millis(100));
2303 }
2304 Err(err) => {
2305 tracing::warn!("Video present failed ({err}), stopping playback");
2306 video_playback.stop();
2307 return;
2308 }
2309 }
2310 }
2311}
2312
2313async fn resolve_targets(
2318 render_states: &std::collections::HashMap<
2319 String,
2320 std::sync::Arc<tokio::sync::Mutex<RenderState>>,
2321 >,
2322 monitor: Option<&str>,
2323) -> Vec<std::sync::Arc<tokio::sync::Mutex<RenderState>>> {
2324 match monitor {
2325 Some(name) => {
2326 if let Some(rs) = render_states.get(name) {
2327 vec![rs.clone()]
2328 } else {
2329 Vec::new()
2330 }
2331 }
2332 None => render_states.values().cloned().collect(),
2333 }
2334}
2335
2336fn resolve_named_targets(
2337 render_states: &std::collections::HashMap<
2338 String,
2339 std::sync::Arc<tokio::sync::Mutex<RenderState>>,
2340 >,
2341 monitor: Option<&str>,
2342) -> Vec<(String, std::sync::Arc<tokio::sync::Mutex<RenderState>>)> {
2343 match monitor {
2344 Some(name) => render_states
2345 .get(name)
2346 .map(|state| vec![(name.to_string(), state.clone())])
2347 .unwrap_or_default(),
2348 None => render_states
2349 .iter()
2350 .map(|(name, state)| (name.clone(), state.clone()))
2351 .collect(),
2352 }
2353}
2354
2355type RenderStateMap = std::collections::HashMap<String, Arc<Mutex<RenderState>>>;
2356type SharedRenderStates = std::sync::Arc<tokio::sync::Mutex<RenderStateMap>>;
2357
2358async fn snapshot_targets(
2362 map: &SharedRenderStates,
2363 monitor: Option<&str>,
2364) -> Vec<Arc<Mutex<RenderState>>> {
2365 let states = map.lock().await;
2366 resolve_targets(&states, monitor).await
2367}
2368
2369async fn snapshot_named_targets(
2370 map: &SharedRenderStates,
2371 monitor: Option<&str>,
2372) -> Vec<(String, Arc<Mutex<RenderState>>)> {
2373 let states = map.lock().await;
2374 resolve_named_targets(&states, monitor)
2375}
2376
2377async fn snapshot_all_named(map: &SharedRenderStates) -> Vec<(String, Arc<Mutex<RenderState>>)> {
2378 let states = map.lock().await;
2379 states
2380 .iter()
2381 .map(|(name, state)| (name.clone(), state.clone()))
2382 .collect()
2383}
2384
2385async fn reap_idle_shm_pools(states: SharedRenderStates) {
2395 loop {
2396 tokio::time::sleep(std::time::Duration::from_millis(250)).await;
2400 for (_, state) in snapshot_all_named(&states).await {
2401 let Ok(mut locked) = state.try_lock() else {
2402 continue;
2403 };
2404 if locked.render_lock.try_lock().is_err() {
2405 continue;
2406 }
2407 if locked.shm_pool.len() > INITIAL_SHM_POOL_BYTES {
2408 locked.shm_buffers = [None, None];
2409 locked.shm_width = 0;
2410 locked.shm_height = 0;
2411 let fresh = SlotPool::new(
2412 INITIAL_SHM_POOL_BYTES,
2413 &smithay_client_toolkit::shm::Shm::from(locked.shm_server.clone()),
2414 );
2415 match fresh {
2416 Ok(pool) => locked.shm_pool = pool,
2417 Err(_) => continue,
2418 }
2419 }
2420 unsafe {
2426 libc::malloc_trim(0);
2427 }
2428 }
2429 }
2430}
2431
2432pub(crate) fn clamp_preload_frames(requested: usize) -> usize {
2437 requested.clamp(1, 3)
2438}
2439
2440pub(crate) fn clamp_max_fps(requested: Option<u32>) -> Option<u32> {
2441 match requested {
2442 Some(0) | None => None,
2443 Some(fps) => Some(fps.clamp(1, 240)),
2444 }
2445}
2446
2447fn is_usable_wallpaper_file(path: &std::path::Path) -> bool {
2451 std::fs::metadata(path).is_ok_and(|m| m.is_file() && m.len() > 0)
2452}
2453
2454fn validate_live_config(cfg: &WallrConfig) -> Result<(), String> {
2458 if cfg.video.preload_frames > 64 {
2459 return Err(format!(
2460 "video.preload_frames {} exceeds maximum 64",
2461 cfg.video.preload_frames
2462 ));
2463 }
2464 if let Some(fps) = cfg.daemon.max_fps
2465 && (fps == 0 || fps > 1000)
2466 {
2467 return Err(format!("daemon.max_fps {fps} out of range 1..=1000"));
2468 }
2469 Ok(())
2470}
2471
2472#[cfg(target_os = "linux")]
2482fn tune_allocator() {
2483 unsafe {
2487 libc::mallopt(libc::M_MMAP_THRESHOLD, 128 * 1024);
2488 libc::mallopt(libc::M_TRIM_THRESHOLD, 64 * 1024);
2489 }
2490}
2491
2492pub struct Daemon {
2493 config: WallrConfig,
2494 config_path: Option<std::path::PathBuf>,
2498 paused: Arc<AtomicBool>,
2499 engine: Arc<Mutex<WallpaperEngine>>,
2500}
2501
2502impl Daemon {
2503 pub fn new(config: WallrConfig) -> Result<Self, DaemonError> {
2504 Self::with_config_path(config, None)
2505 }
2506
2507 pub fn with_config_path(
2508 config: WallrConfig,
2509 config_path: Option<std::path::PathBuf>,
2510 ) -> Result<Self, DaemonError> {
2511 let engine = WallpaperEngine::new(config.clone())?;
2512 Ok(Self {
2513 config,
2514 config_path,
2515 paused: Arc::new(AtomicBool::new(false)),
2516 engine: Arc::new(Mutex::new(engine)),
2517 })
2518 }
2519
2520 pub async fn start(self) -> Result<(), DaemonError> {
2521 tune_allocator();
2522 let socket_path = crate::config::expand_path(&self.config.daemon.socket)?;
2523 let socket = IpcSocket::acquire(&socket_path).await.map_err(|err| {
2527 if matches!(err, crate::ipc::IpcError::AlreadyRunning(_)) {
2528 DaemonError::AlreadyRunning(socket_path.to_string_lossy().to_string())
2529 } else {
2530 DaemonError::Ipc(err)
2531 }
2532 })?;
2533 let socket_identity = socket.identity();
2534
2535 let conn = Connection::connect_to_env()
2536 .map_err(|e| DaemonError::StartError(format!("failed to connect to wayland: {e:?}")))?;
2537 let backend = conn.backend();
2538 let display_ptr = backend.display_ptr() as *mut std::ffi::c_void;
2539
2540 let (globals, mut event_queue) = registry_queue_init(&conn)
2541 .map_err(|e| DaemonError::StartError(format!("registry_queue_init failed: {e:?}")))?;
2542 let qh = event_queue.handle();
2543
2544 let compositor_state = CompositorState::bind(&globals, &qh)
2545 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
2546 let layer_shell = LayerShell::bind(&globals, &qh)
2547 .map_err(|e| DaemonError::StartError(format!("layer_shell bind failed: {e:?}")))?;
2548 let shm = Shm::bind(&globals, &qh)
2549 .map_err(|e| DaemonError::StartError(format!("shm bind failed: {e:?}")))?;
2550
2551 let compositor = globals
2553 .bind::<wl_compositor::WlCompositor, WaylandState, smithay_client_toolkit::globals::GlobalData>(
2554 &qh,
2555 1..=4,
2556 smithay_client_toolkit::globals::GlobalData,
2557 )
2558 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
2559 let viewporter = globals
2560 .bind::<WpViewporter, WaylandState, ()>(&qh, 1..=1, ())
2561 .ok();
2562 if viewporter.is_none() {
2563 tracing::warn!(
2564 "wp_viewporter is unavailable; fractional outputs will use integer buffer scaling"
2565 );
2566 }
2567
2568 let mut wayland_state = WaylandState {
2569 registry_state: RegistryState::new(&globals),
2570 output_state: OutputState::new(&globals, &qh),
2571 compositor_state,
2572 shm,
2573 outputs: std::collections::HashMap::new(),
2574 surfaces: Vec::new(),
2575 viewporter,
2576 viewports: std::collections::HashMap::new(),
2577 output_lifecycles: std::collections::HashMap::new(),
2578 pending_restores: std::collections::HashSet::new(),
2579 layer_shell,
2580 compositor,
2581 hotplug: None,
2582 };
2583
2584 for i in 0..5 {
2588 event_queue
2589 .roundtrip(&mut wayland_state)
2590 .map_err(|e| DaemonError::StartError(format!("roundtrip {i} failed: {e:?}")))?;
2591 }
2592
2593 if wayland_state.outputs.is_empty() {
2594 return Err(DaemonError::StartError(
2595 "no outputs detected after roundtrip".into(),
2596 ));
2597 }
2598
2599 tracing::info!(
2600 "Detected {} output(s): {:?}",
2601 wayland_state.outputs.len(),
2602 wayland_state
2603 .outputs
2604 .values()
2605 .map(|o| format!("{} ({}x{})", o.name, o.width, o.height))
2606 .collect::<Vec<_>>()
2607 );
2608
2609 let renderer: std::sync::Arc<std::sync::Mutex<Option<std::sync::Arc<Renderer>>>> =
2610 std::sync::Arc::new(std::sync::Mutex::new(None));
2611
2612 let mut render_states_map: std::collections::HashMap<String, Arc<Mutex<RenderState>>> =
2616 std::collections::HashMap::new();
2617
2618 let output_info: Vec<(u32, OutputInfo)> = wayland_state
2621 .outputs
2622 .iter()
2623 .map(|(k, v)| {
2624 (
2625 *k,
2626 OutputInfo {
2627 name: v.name.clone(),
2628 width: v.width,
2629 height: v.height,
2630 scale_factor: v.scale_factor,
2631 wl_output: v.wl_output.clone(),
2632 },
2633 )
2634 })
2635 .collect();
2636
2637 for (proto_id, info) in &output_info {
2638 let name = info.name.clone();
2639 let rs = Self::create_render_state_for_output(
2640 &renderer,
2641 display_ptr,
2642 &mut wayland_state,
2643 &qh,
2644 info,
2645 &self.config,
2646 )
2647 .await?;
2648 let rs = Arc::new(Mutex::new(rs));
2649 render_states_map.insert(name.clone(), rs.clone());
2650 wayland_state.output_lifecycles.insert(
2651 *proto_id,
2652 OutputLifecycle {
2653 name: name.clone(),
2654 render_state: rs,
2655 active: Arc::new(std::sync::atomic::AtomicBool::new(true)),
2656 },
2657 );
2658 wayland_state.pending_restores.insert(*proto_id);
2659
2660 tracing::info!("Output ready: {name} ({proto_id})");
2661 }
2662
2663 let render_states: std::sync::Arc<
2666 tokio::sync::Mutex<std::collections::HashMap<String, Arc<Mutex<RenderState>>>>,
2667 > = std::sync::Arc::new(tokio::sync::Mutex::new(render_states_map));
2668
2669 wayland_state.hotplug = Some(DaemonHotplug {
2672 renderer: renderer.clone(),
2673 config: self.config.clone(),
2674 display_ptr: SendDisplayPtr(display_ptr),
2675 render_states: render_states.clone(),
2676 });
2677
2678 event_queue
2681 .roundtrip(&mut wayland_state)
2682 .map_err(|e| DaemonError::StartError(format!("hotplug roundtrip failed: {e:?}")))?;
2683
2684 let paused_clone = self.paused.clone();
2685 let engine_clone = self.engine.clone();
2686 let render_states_clone = render_states.clone();
2687 tokio::spawn(reap_idle_shm_pools(render_states.clone()));
2689 let theme_gen = Arc::new(std::sync::atomic::AtomicU64::new(0));
2693 let theme_gen_clone = theme_gen.clone();
2694
2695 {
2699 let rs_map = render_states.clone();
2700 let socket_path = socket_path.clone();
2701 tokio::spawn(async move {
2702 use tokio::signal::unix::{SignalKind, signal};
2703 let Ok(mut term) = signal(SignalKind::terminate()) else {
2704 return;
2705 };
2706 let Ok(mut int) = signal(SignalKind::interrupt()) else {
2707 return;
2708 };
2709 let Ok(mut hup) = signal(SignalKind::hangup()) else {
2710 return;
2711 };
2712 tokio::select! {
2713 _ = term.recv() => {}
2714 _ = int.recv() => {}
2715 _ = hup.recv() => {}
2716 }
2717 tracing::info!("Signal received, shutting down gracefully");
2718 let states = rs_map.lock().await;
2719 for rs in states.values() {
2720 if let Ok(state) = rs.try_lock() {
2721 state.video_playback.stop();
2722 }
2723 }
2724 drop(states);
2725 remove_socket_if_owned(&socket_path, socket_identity);
2726 std::process::exit(0);
2727 });
2728 }
2729
2730 let ipc_socket_path = socket_path.clone();
2731 let reload_config_path = self.config_path.clone();
2732 socket.spawn(move |cmd| {
2733 let paused = paused_clone.clone();
2734 let engine = engine_clone.clone();
2735 let render_states_map = render_states_clone.clone();
2736 let theme_gen = theme_gen_clone.clone();
2737 let stop_socket = ipc_socket_path.clone();
2738 let stop_identity = socket_identity;
2739 let reload_config_path = reload_config_path.clone();
2740 async move {
2741 match cmd {
2742 IpcCommand::Pause { monitor } => {
2743 let targets =
2744 snapshot_targets(&render_states_map, monitor.as_deref()).await;
2745 if targets.is_empty() {
2746 return IpcResponse {
2747 success: false,
2748 message: Some("no matching outputs".into()),
2749 };
2750 }
2751 if monitor.is_none() {
2752 paused.store(true, Ordering::SeqCst);
2753 }
2754 for rs in targets {
2755 let rs_lock = rs.lock().await;
2756 rs_lock.video_playback.pause();
2757 rs_lock.gif_paused.store(true, Ordering::SeqCst);
2758 }
2759 IpcResponse {
2760 success: true,
2761 message: Some("paused".into()),
2762 }
2763 }
2764 IpcCommand::Resume { monitor } => {
2765 let targets =
2766 snapshot_targets(&render_states_map, monitor.as_deref()).await;
2767 if targets.is_empty() {
2768 return IpcResponse {
2769 success: false,
2770 message: Some("no matching outputs".into()),
2771 };
2772 }
2773 if monitor.is_none() {
2774 paused.store(false, Ordering::SeqCst);
2775 }
2776 for rs in targets {
2777 let rs_lock = rs.lock().await;
2778 rs_lock.video_playback.resume();
2779 rs_lock.gif_paused.store(false, Ordering::SeqCst);
2780 rs_lock.pacer.notify();
2781 }
2782 IpcResponse {
2783 success: true,
2784 message: Some("resumed".into()),
2785 }
2786 }
2787 IpcCommand::Reload => {
2788 let reload_path = reload_config_path.clone();
2792 let fresh = tokio::task::spawn_blocking(move || {
2793 crate::config::load_config(reload_path.as_deref())
2794 .map_err(|e| e.to_string())
2795 .and_then(|cfg| {
2796 validate_live_config(&cfg)?;
2797 Ok(cfg)
2798 })
2799 })
2800 .await;
2801 let fresh = match fresh {
2802 Ok(Ok(cfg)) => cfg,
2803 Ok(Err(reason)) => {
2804 return IpcResponse {
2805 success: false,
2806 message: Some(format!("reload rejected: {reason}")),
2807 };
2808 }
2809 Err(err) => {
2810 return IpcResponse {
2811 success: false,
2812 message: Some(format!("reload task failed: {err}")),
2813 };
2814 }
2815 };
2816 let preload = clamp_preload_frames(fresh.video.preload_frames);
2817 let max_fps = clamp_max_fps(fresh.daemon.max_fps);
2818 let hw_accel =
2819 crate::video::HwAccel::from_config(&fresh.video.hw_decode);
2820 {
2821 let mut eng = engine.lock().await;
2822 eng.config = fresh.clone();
2823 }
2824 let reload_config = fresh.clone();
2825 let reload_result = tokio::task::spawn_blocking(move || {
2826 WallpaperEngine::new(reload_config)?.reload_blocking()
2827 })
2828 .await;
2829 match reload_result {
2830 Ok(Ok(())) => {}
2831 Ok(Err(e)) => {
2832 return IpcResponse {
2833 success: false,
2834 message: Some(e.to_string()),
2835 };
2836 }
2837 Err(e) => {
2838 return IpcResponse {
2839 success: false,
2840 message: Some(format!("reload task failed: {e}")),
2841 };
2842 }
2843 }
2844 let targets = snapshot_all_named(&render_states_map).await;
2847 for (_, rs) in targets {
2848 let mut rs_lock = rs.lock().await;
2849 rs_lock.hw_accel = hw_accel;
2850 rs_lock.preload_frames = preload;
2851 rs_lock.max_fps = max_fps;
2852 }
2853 let _ = theme_gen;
2854 IpcResponse {
2855 success: true,
2856 message: Some("reloaded".into()),
2857 }
2858 }
2859 IpcCommand::Preview {
2860 path,
2861 effect,
2862 duration_ms,
2863 fps,
2864 no_theme,
2865 theme_override,
2866 monitor,
2867 scaling_mode,
2868 } => {
2869 if paused.load(Ordering::SeqCst) {
2870 return IpcResponse {
2871 success: false,
2872 message: Some("daemon is paused".into()),
2873 };
2874 }
2875 let p = std::path::PathBuf::from(&path);
2876 if !is_usable_wallpaper_file(&p) {
2877 return IpcResponse {
2878 success: false,
2879 message: Some(format!("file not found or unreadable: {}", path)),
2880 };
2881 }
2882
2883 let targets =
2887 snapshot_named_targets(&render_states_map, monitor.as_deref()).await;
2888 if targets.is_empty() {
2889 return IpcResponse {
2890 success: false,
2891 message: match &monitor {
2892 Some(name) => Some(format!("unknown monitor: {name}")),
2893 None => Some("no outputs available".into()),
2894 },
2895 };
2896 }
2897
2898 let effect = effect.unwrap_or_else(|| {
2899 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
2900 });
2901 let is_video = crate::video::VideoDecoder::is_video_file(&p);
2905 let duration = duration_ms
2906 .unwrap_or(if is_video { 150 } else { 2000 })
2907 .min(crate::ipc::MAX_DURATION_MS);
2908 let sm = scaling_mode.unwrap_or(crate::config::ScalingMode::Fill);
2909 let scaling_mode_u32 = match sm {
2910 crate::config::ScalingMode::Fill => 0u32,
2911 crate::config::ScalingMode::Fit => 1,
2912 crate::config::ScalingMode::Stretch => 2,
2913 crate::config::ScalingMode::Center => 3,
2914 crate::config::ScalingMode::Tile => 4,
2915 };
2916
2917 let mut last_err = None;
2918 for (name, rs) in &targets {
2919 if let Some(fps) = fps {
2920 rs.lock().await.max_fps = Some(clamp_max_fps(Some(fps)).unwrap());
2921 }
2922 let result = set_wallpaper_with_retry(
2923 rs,
2924 &p,
2925 &effect,
2926 duration,
2927 scaling_mode_u32,
2928 )
2929 .await;
2930
2931 match result {
2932 Err(e) => last_err = Some(format!("render failed: {e}")),
2933 Ok(()) => {
2934 if let Err(err) = persist_wallpaper(name, &p) {
2935 tracing::warn!(
2936 "Wallpaper changed on {name}, but state persistence failed: {err}"
2937 );
2938 }
2939 }
2940 }
2941 }
2942
2943 match last_err {
2944 Some(e) => IpcResponse {
2945 success: false,
2946 message: Some(e),
2947 },
2948 None => {
2949 let generation =
2955 theme_gen.fetch_add(1, Ordering::SeqCst) + 1;
2956 let opts = SetOptions {
2957 no_theme,
2958 theme_provider: theme_override,
2959 monitor,
2960 };
2961 let theme_gen_check = theme_gen.clone();
2962 let p_clone = p.clone();
2963 let config = {
2968 let eng = engine.lock().await;
2969 eng.config.clone()
2970 };
2971 tokio::task::spawn_blocking(move || {
2972 if theme_gen_check.load(Ordering::SeqCst) != generation {
2973 return;
2974 }
2975 let mut eng = match WallpaperEngine::new(config) {
2976 Ok(engine) => engine,
2977 Err(err) => {
2978 tracing::warn!(
2979 "Post-render theme setup failed for {p_clone:?}: {err}"
2980 );
2981 return;
2982 }
2983 };
2984 if let Err(e) =
2985 eng.set_wallpaper_blocking(&p_clone, &opts)
2986 {
2987 tracing::warn!(
2988 "Post-render hooks/theme failed for {p_clone:?}: {e}"
2989 );
2990 }
2991 });
2992 IpcResponse {
2993 success: true,
2994 message: None,
2995 }
2996 }
2997 }
2998 }
2999 IpcCommand::Stop => {
3000 let targets = snapshot_all_named(&render_states_map).await;
3001 for (_, rs) in targets {
3002 let state = rs.lock().await;
3003 state.playback_gen.fetch_add(1, Ordering::SeqCst);
3004 state.pacer.notify();
3005 state.video_playback.stop();
3006 }
3007 let sp = stop_socket.clone();
3008 tokio::spawn(async move {
3009 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
3010 remove_socket_if_owned(&sp, stop_identity);
3011 std::process::exit(0);
3012 });
3013 IpcResponse {
3014 success: true,
3015 message: Some("stopping".into()),
3016 }
3017 }
3018 IpcCommand::Status => {
3019 let state = if paused.load(Ordering::SeqCst) {
3020 "paused"
3021 } else {
3022 "running"
3023 };
3024 IpcResponse {
3025 success: true,
3026 message: Some(format!("wallr daemon {}", state)),
3027 }
3028 }
3029 IpcCommand::Seek {
3030 timestamp_ms,
3031 monitor,
3032 } => {
3033 let targets =
3034 snapshot_named_targets(&render_states_map, monitor.as_deref()).await;
3035 if targets.is_empty() {
3036 return IpcResponse {
3037 success: false,
3038 message: match &monitor {
3039 Some(name) => Some(format!("unknown monitor: {name}")),
3040 None => Some("no outputs available".into()),
3041 },
3042 };
3043 }
3044 let mut seek_count = 0u32;
3048 let mut errors = Vec::new();
3049 for (name, rs) in &targets {
3050 let rs_lock = rs.lock().await;
3051 match rs_lock
3052 .video_playback
3053 .seek(std::time::Duration::from_millis(timestamp_ms))
3054 {
3055 Ok(()) => {
3056 seek_count += 1;
3057 }
3058 Err(e) => {
3059 errors.push(format!("{}: {}", name, e));
3060 }
3061 }
3062 }
3063 if seek_count == 0 {
3064 IpcResponse {
3065 success: false,
3066 message: Some(format!(
3067 "Seek failed on all outputs: {}",
3068 errors.join("; ")
3069 )),
3070 }
3071 } else if !errors.is_empty() {
3072 IpcResponse {
3073 success: true,
3074 message: Some(format!(
3075 "Seeked {} output(s) to {}ms, {} failed: {}",
3076 seek_count,
3077 timestamp_ms,
3078 errors.len(),
3079 errors.join("; ")
3080 )),
3081 }
3082 } else {
3083 IpcResponse {
3084 success: true,
3085 message: Some(format!(
3086 "Seeked {} output(s) to {}ms",
3087 seek_count, timestamp_ms
3088 )),
3089 }
3090 }
3091 }
3092 IpcCommand::Info { monitor } => {
3093 let targets =
3094 snapshot_named_targets(&render_states_map, monitor.as_deref()).await;
3095 if targets.is_empty() {
3096 return IpcResponse {
3097 success: false,
3098 message: match &monitor {
3099 Some(name) => Some(format!("unknown monitor: {name}")),
3100 None => Some("no outputs available".into()),
3101 },
3102 };
3103 }
3104
3105 let mut lines = vec![
3106 format!("wallr v{}", env!("CARGO_PKG_VERSION")),
3107 String::new(),
3108 format!("outputs: {}", targets.len()),
3109 ];
3110 for (name, _) in &targets {
3111 lines.push(format!(" - {name}"));
3112 }
3113
3114 for (name, rs) in &targets {
3116 let rs_lock = rs.lock().await;
3117 let gpu_info = rs_lock.gpu.as_ref().map_or_else(
3121 || "gpu: not initialized (static content)".to_string(),
3122 |gpu| {
3123 crate::video::gpu::adapter_diagnostics(&gpu.renderer.adapter)
3124 },
3125 );
3126
3127 lines.push(String::new());
3128 lines.push(format!("[{name}] {}x{}", rs_lock.width, rs_lock.height));
3129 lines.push(gpu_info);
3130
3131 match rs_lock.video_playback.metadata() {
3132 Some(meta) => {
3133 let decoder_info = rs_lock.video_playback.decoder_info();
3134 let hw = rs_lock.video_playback.hw_accel_in_use();
3135 let state = if rs_lock.video_playback.is_paused() {
3136 "paused"
3137 } else {
3138 "playing"
3139 };
3140 let position = rs_lock
3141 .video_playback
3142 .position()
3143 .map(|p| format!("{:.2}s", p.as_secs_f64()))
3144 .unwrap_or_else(|| "?".to_string());
3145 lines.push(String::new());
3146 lines.push("video:".into());
3147 lines.push(format!(
3148 " resolution: {}x{}",
3149 meta.width, meta.height
3150 ));
3151 lines.push(format!(" fps: {:.2}", meta.fps));
3152 lines.push(format!(
3153 " duration: {:.2}s",
3154 meta.duration.as_secs_f64()
3155 ));
3156 lines.push(format!(
3157 " codec: {}",
3158 decoder_info
3159 .as_ref()
3160 .map(|d| d.codec_name.as_str())
3161 .unwrap_or("unknown")
3162 ));
3163 lines.push(format!(" container: {}", meta.format));
3164 lines.push(format!(" decoder: {}", hw.name()));
3165 if let Some(info) = decoder_info.as_ref() {
3166 lines.push(format!(" decoder state: {}", info.state.name()));
3167 }
3168 lines.push(format!(
3169 " gpu decode: {}",
3170 if hw == crate::video::HwAccel::Software {
3171 "disabled"
3172 } else {
3173 "enabled"
3174 }
3175 ));
3176 if let Some(info) = decoder_info.as_ref()
3177 && info.dropped_frames > 0
3178 {
3179 lines.push(format!(
3180 " dropped frames: {}",
3181 info.dropped_frames
3182 ));
3183 }
3184 lines.push(format!(" state: {} @ {}", state, position));
3185 }
3186 None => {
3187 lines.push(String::new());
3188 lines.push("video: none active".into());
3189 lines.push("decoder: idle".into());
3190 }
3191 }
3192 }
3193
3194 IpcResponse {
3195 success: true,
3196 message: Some(lines.join("\n")),
3197 }
3198 }
3199 IpcCommand::MonitorList => {
3200 let targets = snapshot_all_named(&render_states_map).await;
3201 let mut lines = Vec::new();
3202 for (name, rs) in &targets {
3203 let lock = rs.lock().await;
3204 lines.push(format!("{}: {}x{}", name, lock.width, lock.height));
3205 }
3206 if lines.is_empty() {
3207 IpcResponse {
3208 success: true,
3209 message: Some("no monitors connected".into()),
3210 }
3211 } else {
3212 IpcResponse {
3213 success: true,
3214 message: Some(lines.join("\n")),
3215 }
3216 }
3217 }
3218 IpcCommand::MonitorCurrent => {
3219 let targets = snapshot_all_named(&render_states_map).await;
3221 if let Some((name, rs)) = targets.first() {
3222 let lock = rs.lock().await;
3223 IpcResponse {
3224 success: true,
3225 message: Some(format!("{}: {}x{}", name, lock.width, lock.height)),
3226 }
3227 } else {
3228 IpcResponse {
3229 success: false,
3230 message: Some("no monitors connected".into()),
3231 }
3232 }
3233 }
3234 IpcCommand::Blank {
3235 monitor,
3236 effect,
3237 duration_ms,
3238 } => {
3239 let targets =
3240 snapshot_named_targets(&render_states_map, monitor.as_deref()).await;
3241 if targets.is_empty() {
3242 return IpcResponse {
3243 success: false,
3244 message: match &monitor {
3245 Some(name) => Some(format!("unknown monitor: {name}")),
3246 None => Some("no outputs available".into()),
3247 },
3248 };
3249 }
3250 let mut blanked_count = 0u32;
3251 let mut errors = Vec::new();
3252 let black_effect = effect.unwrap_or_else(|| {
3253 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
3254 });
3255 let duration = duration_ms.unwrap_or(800);
3256 let mut blank_file = match tempfile::Builder::new()
3257 .prefix("wallr_blank-")
3258 .suffix(".png")
3259 .tempfile()
3260 {
3261 Ok(file) => file,
3262 Err(e) => {
3263 return IpcResponse {
3264 success: false,
3265 message: Some(format!("failed to create blank image: {e}")),
3266 };
3267 }
3268 };
3269 let blank_path = blank_file.path().to_path_buf();
3270 let blank = image::DynamicImage::ImageRgba8(
3271 image::RgbaImage::from_pixel(1, 1, image::Rgba([0, 0, 0, 255])),
3272 );
3273 if let Err(e) = blank
3274 .write_to(blank_file.as_file_mut(), image::ImageFormat::Png)
3275 {
3276 return IpcResponse {
3277 success: false,
3278 message: Some(format!("failed to write blank image: {e}")),
3279 };
3280 }
3281
3282 for (name, rs) in &targets {
3283 let rs = Arc::clone(rs);
3284 let blank_path = blank_path.clone();
3285 let black_effect = black_effect.clone();
3286 let blanked = tokio::task::spawn_blocking(move || {
3287 let mut lock = rs.blocking_lock();
3288 if lock.blanked {
3289 return Ok(false);
3290 }
3291 let previous = (
3292 lock.last_wallpaper.clone().unwrap_or_default(),
3293 lock.scaling_mode,
3294 );
3295 lock.set_wallpaper(&blank_path, &black_effect, duration, 0)?;
3296 lock.pre_blank = Some(previous);
3297 lock.blanked = true;
3298 Ok::<bool, anyhow::Error>(true)
3299 })
3300 .await;
3301 match blanked {
3302 Ok(Ok(true)) => blanked_count += 1,
3303 Ok(Ok(false)) => {}
3304 Ok(Err(e)) => errors.push(format!("{name}: blank failed: {e}")),
3305 Err(e) => errors.push(format!("{name}: blank task failed: {e}")),
3306 }
3307 }
3308 if errors.is_empty() {
3309 IpcResponse {
3310 success: true,
3311 message: Some(format!("blanked {blanked_count} output(s)")),
3312 }
3313 } else {
3314 IpcResponse {
3315 success: false,
3316 message: Some(format!(
3317 "blanked {blanked_count} output(s), {} error(s): {}",
3318 errors.len(),
3319 errors.join("; ")
3320 )),
3321 }
3322 }
3323 }
3324 IpcCommand::Restore {
3325 monitor,
3326 effect,
3327 duration_ms,
3328 } => {
3329 let targets =
3330 snapshot_named_targets(&render_states_map, monitor.as_deref()).await;
3331 if targets.is_empty() {
3332 return IpcResponse {
3333 success: false,
3334 message: match &monitor {
3335 Some(name) => Some(format!("unknown monitor: {name}")),
3336 None => Some("no outputs available".into()),
3337 },
3338 };
3339 }
3340 let mut restored_count = 0u32;
3341 let mut errors = Vec::new();
3342 let restore_effect = effect.unwrap_or_else(|| {
3343 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
3344 });
3345 let duration = duration_ms.unwrap_or(800).min(crate::ipc::MAX_DURATION_MS);
3346
3347 for (name, rs) in &targets {
3348 let rs = Arc::clone(rs);
3349 let restore_effect = restore_effect.clone();
3350 let result = tokio::task::spawn_blocking(move || {
3351 let mut lock = rs.blocking_lock();
3352 if !lock.blanked {
3353 return None;
3354 }
3355 let result = match lock.pre_blank.clone() {
3356 Some((path, scaling_mode)) if path.exists() => lock
3357 .set_wallpaper(
3358 &path,
3359 &restore_effect,
3360 duration,
3361 scaling_mode,
3362 )
3363 .map_err(|e| format!("restore failed: {e}")),
3364 Some(_) => Err("wallpaper path no longer exists".to_string()),
3365 None => Err("no previous wallpaper to restore".to_string()),
3366 };
3367 if result.is_ok() {
3368 lock.blanked = false;
3369 lock.pre_blank = None;
3370 }
3371 Some(result)
3372 })
3373 .await;
3374 match result {
3375 Ok(Some(Ok(()))) => restored_count += 1,
3376 Ok(Some(Err(e))) => errors.push(format!("{name}: {e}")),
3377 Ok(None) => {}
3378 Err(e) => errors.push(format!("{name}: restore task failed: {e}")),
3379 }
3380 }
3381 if !errors.is_empty() {
3382 IpcResponse {
3383 success: restored_count > 0,
3384 message: Some(format!(
3385 "restored {} output(s), {} error(s): {}",
3386 restored_count,
3387 errors.len(),
3388 errors.join("; ")
3389 )),
3390 }
3391 } else {
3392 IpcResponse {
3393 success: true,
3394 message: Some(format!("restored {restored_count} output(s)")),
3395 }
3396 }
3397 }
3398 }
3399 }
3400 });
3401
3402 tokio::task::spawn_blocking(move || {
3403 loop {
3404 if let Err(e) = event_queue.blocking_dispatch(&mut wayland_state) {
3405 eprintln!("Wayland dispatch error: {e:?}");
3406 break;
3407 }
3408 }
3409 eprintln!("wallr: Wayland connection lost, exiting");
3413 std::process::exit(1);
3414 });
3415
3416 loop {
3417 tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
3418 }
3419 }
3420
3421 #[allow(clippy::too_many_arguments)]
3424 async fn create_render_state_for_output(
3425 renderer: &std::sync::Arc<std::sync::Mutex<Option<std::sync::Arc<Renderer>>>>,
3426 display_ptr: *mut std::ffi::c_void,
3427 wayland_state: &mut WaylandState,
3428 qh: &QueueHandle<WaylandState>,
3429 output: &OutputInfo,
3430 config: &WallrConfig,
3431 ) -> Result<RenderState, DaemonError> {
3432 let wl_surface = wayland_state.compositor_state.create_surface(qh);
3433 let layer_surface = wayland_state.layer_shell.create_layer_surface(
3434 qh,
3435 wl_surface,
3436 Layer::Background,
3437 Some("wallr"),
3438 Some(&output.wl_output),
3439 );
3440 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
3441 layer_surface.set_exclusive_zone(-1);
3442 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
3443
3444 let empty_region = wayland_state.compositor.create_region(qh, ());
3446 layer_surface
3447 .wl_surface()
3448 .set_input_region(Some(&empty_region));
3449 let output_id = output.wl_output.id().protocol_id();
3450 let viewport = wayland_state
3451 .viewporter
3452 .as_ref()
3453 .map(|viewporter| viewporter.get_viewport(layer_surface.wl_surface(), qh, ()));
3454 let scale_factor = if viewport.is_some() {
3455 1
3456 } else if output.scale_factor > 0 {
3457 output.scale_factor
3458 } else {
3459 1
3460 };
3461 layer_surface.wl_surface().set_buffer_scale(scale_factor);
3462 layer_surface.commit();
3463 empty_region.destroy();
3464 if let Some(viewport) = viewport {
3465 wayland_state.viewports.insert(output_id, viewport);
3466 }
3467
3468 let width = output.width;
3470 let height = output.height;
3471
3472 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
3473 let shm_surface = layer_surface.wl_surface().clone();
3474 wayland_state.surfaces.push((output_id, layer_surface));
3475
3476 Ok(RenderState {
3477 gpu: None,
3478 renderer: renderer.clone(),
3479 display_ptr: SendDisplayPtr(display_ptr),
3480 raw_surface: SendDisplayPtr(raw_surface),
3481 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
3482 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
3483 pacer: std::sync::Arc::new(LivePacer::new()),
3484 current_bind: None,
3485 current_tex: None,
3486 width,
3487 height,
3488 current_width: 0,
3489 current_height: 0,
3490 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
3491 hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
3492 preferred_gpu: config.video.preferred_gpu.clone(),
3493 preload_frames: clamp_preload_frames(config.video.preload_frames),
3494 max_fps: clamp_max_fps(config.daemon.max_fps),
3495 scaling_mode: 0,
3496 current_effect: None,
3497 last_wallpaper: None,
3498 pre_blank: None,
3499 blanked: false,
3500 gif_paused: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3501 shm_surface,
3502 shm_server: wayland_state.shm.wl_shm().clone(),
3503 shm_pool: SlotPool::new(INITIAL_SHM_POOL_BYTES, &wayland_state.shm).map_err(|e| {
3504 DaemonError::StartError(format!("shared-memory pool creation failed: {e}"))
3505 })?,
3506 shm_buffers: [None, None],
3507 shm_index: 0,
3508 shm_width: 0,
3509 shm_height: 0,
3510 cached_shm_image: None,
3511 gpu_surface_used: false,
3512 cached_video_texture: None,
3513 loop_video: config.wallpaper.loop_video,
3514 })
3515 }
3516}
3517
3518#[allow(clippy::too_many_arguments)]
3522fn create_render_state_for_output_sync(
3523 renderer: &std::sync::Arc<std::sync::Mutex<Option<std::sync::Arc<Renderer>>>>,
3524 display_ptr: *mut std::ffi::c_void,
3525 wayland_state: &mut WaylandState,
3526 qh: &QueueHandle<WaylandState>,
3527 output: &OutputInfo,
3528 config: &WallrConfig,
3529) -> Result<RenderState, DaemonError> {
3530 let wl_surface = wayland_state.compositor_state.create_surface(qh);
3531 let layer_surface = wayland_state.layer_shell.create_layer_surface(
3532 qh,
3533 wl_surface,
3534 Layer::Background,
3535 Some("wallr"),
3536 Some(&output.wl_output),
3537 );
3538 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
3539 layer_surface.set_exclusive_zone(-1);
3540 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
3541
3542 let empty_region = wayland_state.compositor.create_region(qh, ());
3543 layer_surface
3544 .wl_surface()
3545 .set_input_region(Some(&empty_region));
3546 let output_id = output.wl_output.id().protocol_id();
3547 let viewport = wayland_state
3548 .viewporter
3549 .as_ref()
3550 .map(|viewporter| viewporter.get_viewport(layer_surface.wl_surface(), qh, ()));
3551 let scale_factor = if viewport.is_some() {
3552 1
3553 } else if output.scale_factor > 0 {
3554 output.scale_factor
3555 } else {
3556 1
3557 };
3558 layer_surface.wl_surface().set_buffer_scale(scale_factor);
3559 layer_surface.commit();
3560 empty_region.destroy();
3561 if let Some(viewport) = viewport {
3562 wayland_state.viewports.insert(output_id, viewport);
3563 }
3564
3565 let width = output.width;
3567 let height = output.height;
3568
3569 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
3570 let shm_surface = layer_surface.wl_surface().clone();
3571 wayland_state.surfaces.push((output_id, layer_surface));
3572
3573 Ok(RenderState {
3574 gpu: None,
3575 renderer: renderer.clone(),
3576 display_ptr: SendDisplayPtr(display_ptr),
3577 raw_surface: SendDisplayPtr(raw_surface),
3578 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
3579 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
3580 pacer: std::sync::Arc::new(LivePacer::new()),
3581 current_bind: None,
3582 current_tex: None,
3583 width,
3584 height,
3585 current_width: 0,
3586 current_height: 0,
3587 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
3588 hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
3589 preferred_gpu: config.video.preferred_gpu.clone(),
3590 preload_frames: clamp_preload_frames(config.video.preload_frames),
3591 max_fps: clamp_max_fps(config.daemon.max_fps),
3592 scaling_mode: 0,
3593 current_effect: None,
3594 last_wallpaper: None,
3595 pre_blank: None,
3596 blanked: false,
3597 gif_paused: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3598 shm_surface,
3599 shm_server: wayland_state.shm.wl_shm().clone(),
3600 shm_pool: SlotPool::new(INITIAL_SHM_POOL_BYTES, &wayland_state.shm).map_err(|e| {
3601 DaemonError::StartError(format!("shared-memory pool creation failed: {e}"))
3602 })?,
3603 shm_buffers: [None, None],
3604 shm_index: 0,
3605 shm_width: 0,
3606 shm_height: 0,
3607 cached_shm_image: None,
3608 gpu_surface_used: false,
3609 cached_video_texture: None,
3610 loop_video: config.wallpaper.loop_video,
3611 })
3612}