1use crate::config::WallrConfig;
2use crate::ipc::{IpcCommand, IpcResponse, start_ipc_server};
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_compositor, delegate_layer, delegate_output, delegate_registry, delegate_shm,
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
52pub struct WaylandWindow {
53 pub display: *mut std::ffi::c_void,
54 pub surface: *mut std::ffi::c_void,
55}
56
57unsafe impl Send for WaylandWindow {}
58unsafe impl Sync for WaylandWindow {}
59
60impl HasWindowHandle for WaylandWindow {
61 fn window_handle(&self) -> Result<WindowHandle<'_>, raw_window_handle::HandleError> {
62 let surface = std::ptr::NonNull::new(self.surface)
63 .ok_or(raw_window_handle::HandleError::Unavailable)?;
64 let handle = WaylandWindowHandle::new(surface);
65 unsafe { Ok(WindowHandle::borrow_raw(RawWindowHandle::Wayland(handle))) }
66 }
67}
68
69impl HasDisplayHandle for WaylandWindow {
70 fn display_handle(&self) -> Result<DisplayHandle<'_>, raw_window_handle::HandleError> {
71 let display = std::ptr::NonNull::new(self.display)
72 .ok_or(raw_window_handle::HandleError::Unavailable)?;
73 let handle = WaylandDisplayHandle::new(display);
74 unsafe { Ok(DisplayHandle::borrow_raw(RawDisplayHandle::Wayland(handle))) }
75 }
76}
77
78#[derive(Clone)]
79struct OutputInfo {
80 name: String,
81 width: u32,
82 height: u32,
83 scale_factor: i32,
84 wl_output: wl_output::WlOutput,
85}
86
87#[derive(Clone)]
88struct OutputLifecycle {
89 name: String,
90 render_state: std::sync::Arc<tokio::sync::Mutex<RenderState>>,
91 active: std::sync::Arc<std::sync::atomic::AtomicBool>,
92}
93
94struct WaylandState {
95 registry_state: RegistryState,
96 output_state: OutputState,
97 compositor_state: CompositorState,
98 shm: Shm,
99 outputs: std::collections::HashMap<u32, OutputInfo>,
100 surfaces: Vec<(u32, LayerSurface)>,
101 viewporter: Option<WpViewporter>,
102 viewports: std::collections::HashMap<u32, WpViewport>,
103 output_lifecycles: std::collections::HashMap<u32, OutputLifecycle>,
104 pending_restores: std::collections::HashSet<u32>,
105 layer_shell: LayerShell,
107 compositor: wl_compositor::WlCompositor,
109 hotplug: Option<DaemonHotplug>,
112}
113
114struct SendDisplayPtr(*mut std::ffi::c_void);
117unsafe impl Send for SendDisplayPtr {}
118
119struct DaemonHotplug {
123 renderer: std::sync::Arc<std::sync::Mutex<Option<std::sync::Arc<Renderer>>>>,
124 config: crate::config::WallrConfig,
125 display_ptr: SendDisplayPtr,
126 render_states: std::sync::Arc<
131 tokio::sync::Mutex<
132 std::collections::HashMap<String, std::sync::Arc<tokio::sync::Mutex<RenderState>>>,
133 >,
134 >,
135}
136
137impl ProvidesRegistryState for WaylandState {
138 fn registry(&mut self) -> &mut RegistryState {
139 &mut self.registry_state
140 }
141
142 registry_handlers![OutputState,];
143}
144
145impl CompositorHandler for WaylandState {
146 fn scale_factor_changed(
147 &mut self,
148 _conn: &Connection,
149 _qh: &QueueHandle<Self>,
150 _surface: &wl_surface::WlSurface,
151 _new_factor: i32,
152 ) {
153 }
154 fn transform_changed(
155 &mut self,
156 _conn: &Connection,
157 _qh: &QueueHandle<Self>,
158 _surface: &wl_surface::WlSurface,
159 _new_transform: wl_output::Transform,
160 ) {
161 }
162 fn frame(
163 &mut self,
164 _conn: &Connection,
165 _qh: &QueueHandle<Self>,
166 _surface: &wl_surface::WlSurface,
167 _time: u32,
168 ) {
169 }
170 fn surface_enter(
171 &mut self,
172 _conn: &Connection,
173 _qh: &QueueHandle<Self>,
174 _surface: &wl_surface::WlSurface,
175 _output: &wl_output::WlOutput,
176 ) {
177 }
178 fn surface_leave(
179 &mut self,
180 _conn: &Connection,
181 _qh: &QueueHandle<Self>,
182 _surface: &wl_surface::WlSurface,
183 _output: &wl_output::WlOutput,
184 ) {
185 }
186}
187
188impl wayland_client::Dispatch<wayland_client::protocol::wl_region::WlRegion, ()> for WaylandState {
189 fn event(
190 _state: &mut WaylandState,
191 _region: &wayland_client::protocol::wl_region::WlRegion,
192 _event: wayland_client::protocol::wl_region::Event,
193 _data: &(),
194 _conn: &Connection,
195 _qh: &QueueHandle<WaylandState>,
196 ) {
197 }
198}
199
200impl Dispatch<WpViewporter, ()> for WaylandState {
201 fn event(
202 _state: &mut WaylandState,
203 _proxy: &WpViewporter,
204 _event: wp_viewporter::Event,
205 _data: &(),
206 _conn: &Connection,
207 _qh: &QueueHandle<WaylandState>,
208 ) {
209 }
210}
211
212impl Dispatch<WpViewport, ()> for WaylandState {
213 fn event(
214 _state: &mut WaylandState,
215 _proxy: &WpViewport,
216 _event: wp_viewport::Event,
217 _data: &(),
218 _conn: &Connection,
219 _qh: &QueueHandle<WaylandState>,
220 ) {
221 }
222}
223
224fn viewport_destination(configured: (u32, u32), physical: (u32, u32)) -> Option<(i32, i32)> {
225 let (width, height) = configured;
226 let (physical_width, physical_height) = physical;
227 match (width, height) {
228 (0, 0) => None,
229 (0, height) if physical_height > 0 => {
230 let width = (u64::from(height) * u64::from(physical_width)
231 + u64::from(physical_height) / 2)
232 / u64::from(physical_height);
233 Some((i32::try_from(width).ok()?, i32::try_from(height).ok()?))
234 }
235 (width, 0) if physical_width > 0 => {
236 let height = (u64::from(width) * u64::from(physical_height)
237 + u64::from(physical_width) / 2)
238 / u64::from(physical_width);
239 Some((i32::try_from(width).ok()?, i32::try_from(height).ok()?))
240 }
241 (width, height) => Some((i32::try_from(width).ok()?, i32::try_from(height).ok()?)),
242 }
243}
244
245#[cfg(test)]
246mod viewport_tests {
247 use super::{
248 VideoPresentAction, advance_gif_slots, clamp_max_fps, clamp_preload_frames,
249 is_transient_wallpaper_error, is_usable_wallpaper_file, persist_wallpaper_at,
250 read_wallpaper_state, validate_live_config, video_present_action, viewport_destination,
251 write_wallpaper_state,
252 };
253 use crate::renderer::FrameStatus;
254
255 #[test]
256 fn gif_slots_never_flip_to_an_unuploaded_frame() {
257 let advanced = advance_gif_slots(0, 0, 1, 1, true);
259 assert_eq!((advanced.0, advanced.1, advanced.3), (1, 1, true));
260
261 let retry = advance_gif_slots(0, 0, 2, 1, false);
263 assert_eq!((retry.0, retry.1, retry.3), (0, 0, false));
264
265 let retried = advance_gif_slots(0, 0, 2, 1, true);
267 assert_eq!((retried.0, retried.1, retried.3), (1, 1, true));
268
269 let same = advance_gif_slots(1, 1, 2, 1, true);
271 assert_eq!((same.0, same.1, same.3), (1, 1, true));
272
273 let jump = advance_gif_slots(1, 1, 2, 4, true);
276 assert_eq!((jump.0, jump.1, jump.3), (0, 4, true));
277 }
278
279 #[test]
280 fn preserves_complete_configure_size() {
281 assert_eq!(
282 viewport_destination((3072, 1728), (3840, 2160)),
283 Some((3072, 1728))
284 );
285 }
286
287 #[test]
288 fn derives_missing_dimension_from_physical_aspect_ratio() {
289 assert_eq!(
290 viewport_destination((0, 1728), (3840, 2160)),
291 Some((3072, 1728))
292 );
293 assert_eq!(
294 viewport_destination((3072, 0), (3840, 2160)),
295 Some((3072, 1728))
296 );
297 assert_eq!(viewport_destination((0, 0), (3840, 2160)), None);
298 }
299
300 #[test]
301 fn retries_recoverable_video_surface_failures() {
302 assert_eq!(
303 video_present_action(FrameStatus::TimedOut),
304 VideoPresentAction::Retry
305 );
306 assert_eq!(
307 video_present_action(FrameStatus::Outdated),
308 VideoPresentAction::Reconfigure
309 );
310 assert_eq!(
311 video_present_action(FrameStatus::Lost),
312 VideoPresentAction::Reconfigure
313 );
314 }
315
316 #[test]
317 fn rotates_wallpaper_state_only_after_successful_persistence() {
318 let temporary = tempfile::tempdir().expect("temporary directory");
319 let first = temporary.path().join("first.jpg");
320 let second = temporary.path().join("second.jpg");
321 std::fs::write(&first, b"first").expect("first wallpaper");
322 std::fs::write(&second, b"second").expect("second wallpaper");
323
324 persist_wallpaper_at(temporary.path(), "DP-1", &first).expect("persist first");
325 assert_eq!(
326 read_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1"),
327 Some(first.clone())
328 );
329 assert_eq!(
330 read_wallpaper_state(temporary.path(), "previous_wallpaper", "DP-1"),
331 None
332 );
333
334 persist_wallpaper_at(temporary.path(), "DP-1", &second).expect("persist second");
335 assert_eq!(
336 read_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1"),
337 Some(second)
338 );
339 assert_eq!(
340 read_wallpaper_state(temporary.path(), "previous_wallpaper", "DP-1"),
341 Some(first)
342 );
343
344 let missing = temporary.path().join("missing.jpg");
345 write_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1", &missing)
346 .expect("persist missing path for restore test");
347 assert_eq!(
348 read_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1"),
349 Some(missing)
350 );
351 }
352
353 #[test]
354 fn wallpaper_state_preserves_non_utf8_and_whitespace() {
355 use std::os::unix::ffi::OsStringExt;
356
357 let temporary = tempfile::tempdir().expect("temporary directory");
358 let wallpaper = std::path::PathBuf::from(std::ffi::OsString::from_vec(
359 b" /tmp/wallpaper-\xff.jpg ".to_vec(),
360 ));
361
362 write_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1", &wallpaper)
363 .expect("persist wallpaper path");
364
365 assert_eq!(
366 read_wallpaper_state(temporary.path(), "last_wallpaper", "DP-1"),
367 Some(wallpaper)
368 );
369 }
370
371 #[test]
372 fn retries_only_explicitly_transient_errors() {
373 let timed_out = anyhow::Error::new(std::io::Error::new(
374 std::io::ErrorKind::TimedOut,
375 "temporary timeout",
376 ));
377 let missing = anyhow::Error::new(std::io::Error::new(
378 std::io::ErrorKind::NotFound,
379 "missing wallpaper",
380 ));
381 let recoverable_video = anyhow::Error::new(crate::video::VideoError::QueueFull);
382
383 assert!(is_transient_wallpaper_error(&timed_out));
384 assert!(is_transient_wallpaper_error(&recoverable_video));
385 assert!(!is_transient_wallpaper_error(&missing));
386 assert!(!is_transient_wallpaper_error(&anyhow::anyhow!(
387 "invalid dimensions"
388 )));
389 }
390
391 #[test]
392 fn live_video_params_stay_bounded() {
393 assert_eq!(clamp_preload_frames(0), 1);
394 assert_eq!(clamp_preload_frames(2), 2);
395 assert_eq!(clamp_preload_frames(100), 3);
396 assert_eq!(clamp_max_fps(None), None);
397 assert_eq!(clamp_max_fps(Some(0)), None);
398 assert_eq!(clamp_max_fps(Some(60)), Some(60));
399 assert_eq!(clamp_max_fps(Some(10_000)), Some(240));
400 }
401
402 #[test]
403 fn watcher_requires_non_empty_files() {
404 let temporary = tempfile::tempdir().expect("temporary directory");
405 let empty = temporary.path().join("empty.jpg");
406 let filled = temporary.path().join("filled.jpg");
407 std::fs::write(&empty, b"").expect("empty file");
408 std::fs::write(&filled, b"data").expect("filled file");
409 assert!(!is_usable_wallpaper_file(&empty));
410 assert!(is_usable_wallpaper_file(&filled));
411 assert!(!is_usable_wallpaper_file(
412 &temporary.path().join("missing.jpg")
413 ));
414 }
415
416 #[test]
417 fn reload_rejects_absurd_live_values() {
418 let mut cfg = crate::config::WallrConfig::default();
419 assert!(validate_live_config(&cfg).is_ok());
420 cfg.video.preload_frames = 1000;
421 assert!(validate_live_config(&cfg).is_err());
422 cfg.video.preload_frames = 2;
423 cfg.daemon.max_fps = Some(0);
424 assert!(validate_live_config(&cfg).is_err());
425 }
426}
427
428impl LayerShellHandler for WaylandState {
429 fn configure(
430 &mut self,
431 _conn: &Connection,
432 _qh: &QueueHandle<Self>,
433 layer: &LayerSurface,
434 configure: LayerSurfaceConfigure,
435 _serial: u32,
436 ) {
437 let output_id = self.surfaces.iter().find_map(|(output_id, surface)| {
440 (surface.wl_surface().id() == layer.wl_surface().id()).then_some(*output_id)
441 });
442 let Some(output_id) = output_id else {
443 return;
444 };
445 let destination = self.outputs.get(&output_id).and_then(|output| {
446 viewport_destination(configure.new_size, (output.width, output.height))
447 });
448 if let Some(viewport) = self.viewports.get(&output_id) {
449 let Some((logical_width, logical_height)) = destination else {
450 tracing::warn!(
451 "Output {output_id} configure omitted both dimensions; waiting for a usable size"
452 );
453 return;
454 };
455 viewport.set_destination(logical_width, logical_height);
456 tracing::debug!(
457 "Configured viewport destination for output {output_id}: {logical_width}x{logical_height}"
458 );
459 }
460 if !self.pending_restores.remove(&output_id) {
461 return;
462 }
463 let Some(lifecycle) = self.output_lifecycles.get(&output_id).cloned() else {
464 return;
465 };
466 let Some(render_states) = self
467 .hotplug
468 .as_ref()
469 .map(|hotplug| hotplug.render_states.clone())
470 else {
471 return;
472 };
473
474 tokio::spawn(async move {
475 if !lifecycle.active.load(Ordering::SeqCst) {
476 return;
477 }
478 restore_cached_wallpaper(&lifecycle.name, &lifecycle.render_state).await;
479
480 let mut states = render_states.lock().await;
481 if lifecycle.active.load(Ordering::SeqCst) {
482 states.insert(lifecycle.name.clone(), lifecycle.render_state);
483 tracing::info!("Output configured: {}", lifecycle.name);
484 }
485 });
486 }
487
488 fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {}
489}
490
491impl ShmHandler for WaylandState {
492 fn shm_state(&mut self) -> &mut Shm {
493 &mut self.shm
494 }
495}
496
497impl OutputHandler for WaylandState {
498 fn output_state(&mut self) -> &mut OutputState {
499 &mut self.output_state
500 }
501 fn new_output(
502 &mut self,
503 _conn: &Connection,
504 _qh: &QueueHandle<Self>,
505 output: wl_output::WlOutput,
506 ) {
507 let id = output.id().protocol_id();
508 tracing::info!("Output detected: protocol_id={id}");
509
510 let mut info = OutputInfo {
511 name: format!("output-{id}"),
512 width: 1920,
513 height: 1080,
514 scale_factor: 1,
515 wl_output: output,
516 };
517
518 if let Some(info_data) = self.output_state.info(&info.wl_output) {
520 if let Some(mode) = info_data.modes.iter().find(|m| m.current) {
521 info.width = mode.dimensions.0 as u32;
522 info.height = mode.dimensions.1 as u32;
523 }
524 info.scale_factor = info_data.scale_factor;
525 let resolved = info_data
526 .name
527 .as_deref()
528 .filter(|n| !n.is_empty())
529 .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
530 if let Some(real_name) = resolved {
531 info.name = real_name.to_string();
532 } else if !info_data.make.is_empty() || !info_data.model.is_empty() {
533 let fallback = format!("{} {}", info_data.make, info_data.model)
534 .trim()
535 .to_string();
536 if !fallback.is_empty() {
537 info.name = fallback;
538 }
539 }
540 }
541
542 self.outputs.insert(id, info.clone());
543
544 if self.hotplug.is_some() {
548 let name = info.name.clone();
549 let renderer = self.hotplug.as_ref().unwrap().renderer.clone();
550 let display_ptr = self.hotplug.as_ref().unwrap().display_ptr.0;
551 let config = self.hotplug.as_ref().unwrap().config.clone();
552 match create_render_state_for_output_sync(
553 &renderer,
554 display_ptr,
555 self,
556 _qh,
557 &info,
558 &config,
559 ) {
560 Ok(rs) => {
561 let rs = std::sync::Arc::new(tokio::sync::Mutex::new(rs));
562 self.output_lifecycles.insert(
563 id,
564 OutputLifecycle {
565 name,
566 render_state: rs,
567 active: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)),
568 },
569 );
570 self.pending_restores.insert(id);
571 }
572 Err(e) => {
573 tracing::error!("Hotplug: failed to create render state for {name}: {e}");
574 }
575 }
576 }
577 }
578 fn update_output(
579 &mut self,
580 _conn: &Connection,
581 _qh: &QueueHandle<Self>,
582 output: wl_output::WlOutput,
583 ) {
584 let id = output.id().protocol_id();
585 if let Some(info) = self.outputs.get_mut(&id) {
586 let mut new_width = info.width;
587 let mut new_height = info.height;
588 let mut new_scale = info.scale_factor;
589
590 if let Some(mode) = self
591 .output_state
592 .info(&output)
593 .and_then(|i| i.modes.iter().find(|m| m.current).cloned())
594 {
595 new_width = mode.dimensions.0 as u32;
596 new_height = mode.dimensions.1 as u32;
597 }
598 if let Some(info_data) = self.output_state.info(&output) {
599 new_scale = info_data.scale_factor;
600 let resolved = info_data
601 .name
602 .as_deref()
603 .filter(|n| !n.is_empty())
604 .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
605 if let Some(real_name) = resolved {
606 if real_name != info.name {
607 tracing::info!(
608 "Output {id}: resolved name '{}' -> '{}'",
609 info.name,
610 real_name
611 );
612 info.name = real_name.to_string();
613 }
614 } else if info.name.starts_with("output-")
615 && (!info_data.make.is_empty() || !info_data.model.is_empty())
616 {
617 let fallback = format!("{} {}", info_data.make, info_data.model)
618 .trim()
619 .to_string();
620 if !fallback.is_empty() {
621 tracing::info!(
622 "Output {id}: fallback name '{}' -> '{}'",
623 info.name,
624 fallback
625 );
626 info.name = fallback;
627 }
628 }
629 }
630
631 let changed = new_width != info.width
632 || new_height != info.height
633 || new_scale != info.scale_factor;
634
635 info.width = new_width;
636 info.height = new_height;
637 info.scale_factor = new_scale;
638
639 if changed {
641 let name = info.name.clone();
642 if let Some(ref hotplug) = self.hotplug {
643 let render_states = hotplug.render_states.clone();
644 tokio::spawn(async move {
645 let states = render_states.lock().await;
646 if let Some(rs) = states.get(&name) {
647 let mut lock = rs.lock().await;
648 lock.width = new_width;
649 lock.height = new_height;
650 if let Some(gpu) = lock.gpu.as_ref() {
651 if let Ok(shared) = lock.renderer.lock() {
652 if let Some(renderer) = shared.as_ref() {
653 gpu.surface.configure(
654 &renderer.device,
655 &wgpu::SurfaceConfiguration {
656 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
657 format: gpu.format,
658 width: new_width,
659 height: new_height,
660 present_mode: wgpu::PresentMode::Fifo,
661 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
662 view_formats: vec![],
663 desired_maximum_frame_latency: 1,
664 },
665 );
666 }
667 }
668 tracing::info!(
669 "Hotplug: reconfigured {name} to {new_width}x{new_height}"
670 );
671 }
672 }
673 });
674 }
675 }
676 }
677 }
678 fn output_destroyed(
679 &mut self,
680 _conn: &Connection,
681 _qh: &QueueHandle<Self>,
682 output: wl_output::WlOutput,
683 ) {
684 let id = output.id().protocol_id();
685 if let Some(info) = self.outputs.remove(&id) {
686 tracing::info!("Output disconnected: {} (protocol_id={})", info.name, id);
687 self.pending_restores.remove(&id);
688 let viewport = self.viewports.remove(&id);
689 let layer_surface = self
690 .surfaces
691 .iter()
692 .position(|(pid, _)| *pid == id)
693 .map(|position| self.surfaces.swap_remove(position).1);
694 let lifecycle = self.output_lifecycles.remove(&id);
695 if let Some(lifecycle) = &lifecycle {
696 lifecycle.active.store(false, Ordering::SeqCst);
697 }
698 if let (Some(hotplug), Some(lifecycle)) = (&self.hotplug, lifecycle) {
700 let render_states = hotplug.render_states.clone();
701 tokio::spawn(async move {
702 let mut states = render_states.lock().await;
703 if states
704 .get(&lifecycle.name)
705 .is_some_and(|state| Arc::ptr_eq(state, &lifecycle.render_state))
706 {
707 states.remove(&lifecycle.name);
708 }
709 drop(states);
710
711 let state = lifecycle.render_state.lock().await;
712 state.playback_gen.fetch_add(1, Ordering::SeqCst);
713 state.pacer.notify();
714 state.video_playback.stop();
715 let render_lock = state.render_lock.clone();
716 drop(state);
717
718 let _ = tokio::task::spawn_blocking(move || {
719 drop(
720 render_lock
721 .lock()
722 .unwrap_or_else(|poisoned| poisoned.into_inner()),
723 );
724 })
725 .await;
726 if let Some(viewport) = viewport {
727 viewport.destroy();
728 }
729 drop(layer_surface);
730 tracing::info!("Hotplug: cleaned up render state for {}", lifecycle.name);
731 });
732 } else {
733 if let Some(viewport) = viewport {
734 viewport.destroy();
735 }
736 drop(layer_surface);
737 }
738 }
739 }
740}
741
742delegate_compositor!(WaylandState);
743delegate_layer!(WaylandState);
744delegate_output!(WaylandState);
745delegate_registry!(WaylandState);
746delegate_shm!(WaylandState);
747
748struct LivePacer {
750 lock: std::sync::Mutex<()>,
751 cond: std::sync::Condvar,
752}
753
754impl LivePacer {
755 fn new() -> Self {
756 Self {
757 lock: std::sync::Mutex::new(()),
758 cond: std::sync::Condvar::new(),
759 }
760 }
761
762 fn notify(&self) {
763 let _guard = self.lock.lock().unwrap();
764 self.cond.notify_all();
765 }
766
767 fn wait_until(&self, deadline: std::time::Instant) {
770 let guard = self.lock.lock().unwrap();
771 let now = std::time::Instant::now();
772 if deadline <= now {
773 return;
774 }
775 let _ = self.cond.wait_timeout(guard, deadline - now);
783 }
784}
785
786struct RenderState {
787 gpu: Option<GpuState>,
791 renderer: std::sync::Arc<std::sync::Mutex<Option<std::sync::Arc<Renderer>>>>,
792 display_ptr: SendDisplayPtr,
793 raw_surface: SendDisplayPtr,
794 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
798 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
801 pacer: std::sync::Arc<LivePacer>,
804 current_bind: Option<wgpu::BindGroup>,
805 current_tex: Option<wgpu::Texture>,
806 width: u32,
807 height: u32,
808 current_width: u32,
809 current_height: u32,
810 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
812 hw_accel: crate::video::HwAccel,
814 preferred_gpu: crate::video::GpuSelection,
816 preload_frames: usize,
818 max_fps: Option<u32>,
820 scaling_mode: u32,
822 current_effect: Option<crate::animation::Effect>,
824 last_wallpaper: Option<std::path::PathBuf>,
827 pre_blank: Option<(std::path::PathBuf, u32)>,
829 blanked: bool,
831 gif_paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
833 shm_surface: wl_surface::WlSurface,
837 shm_server: wayland_client::protocol::wl_shm::WlShm,
840 shm_pool: SlotPool,
841 shm_buffers: [Option<ShmBuffer>; 2],
846 shm_index: usize,
847 shm_width: u32,
848 shm_height: u32,
849 gpu_surface_used: bool,
854 cached_video_texture: Option<std::sync::Arc<crate::renderer::VideoTexture>>,
859 loop_video: bool,
861}
862
863struct GpuState {
864 renderer: std::sync::Arc<Renderer>,
865 surface: &'static wgpu::Surface<'static>,
866 format: wgpu::TextureFormat,
867 per_output_uniforms: std::sync::Arc<crate::renderer::PerOutputUniforms>,
868}
869
870struct CommitData {
873 bg_bind: wgpu::BindGroup,
874 new_bind: wgpu::BindGroup,
875 img_width: u32,
876 img_height: u32,
877 old_img_width: u32,
878 old_img_height: u32,
879 format: wgpu::TextureFormat,
880 width: u32,
881 height: u32,
882 animated: Option<crate::animated::AnimatedImage>,
885 is_video: bool,
887 video_texture: Option<std::sync::Arc<crate::renderer::VideoTexture>>,
890 generation: u64,
893 scaling_mode: u32,
895 max_fps: Option<u32>,
897}
898
899impl RenderState {
900 fn ensure_gpu(&mut self) -> anyhow::Result<()> {
901 if self.gpu.is_some() {
902 return Ok(());
903 }
904
905 let renderer = {
906 let mut shared = self
907 .renderer
908 .lock()
909 .map_err(|_| anyhow::anyhow!("renderer initialization lock poisoned"))?;
910 if let Some(renderer) = shared.as_ref() {
911 renderer.clone()
912 } else {
913 let renderer = tokio::runtime::Handle::current()
914 .block_on(Renderer::new(&self.preferred_gpu))
915 .map_err(|e| anyhow::anyhow!("GPU init failed: {e}"))?;
916 let renderer = std::sync::Arc::new(renderer);
917 *shared = Some(renderer.clone());
918 renderer
919 }
920 };
921
922 let window_handle = WaylandWindow {
923 display: self.display_ptr.0,
924 surface: self.raw_surface.0,
925 };
926 let wgpu_surface = renderer
927 .instance
928 .create_surface(&window_handle)
929 .map_err(|e| anyhow::anyhow!("wgpu surface creation failed: {e:?}"))?;
930 let formats = wgpu_surface.get_capabilities(&renderer.adapter).formats;
931 let format = formats
932 .iter()
933 .copied()
934 .find(|format| *format == wgpu::TextureFormat::Bgra8UnormSrgb)
935 .or_else(|| formats.into_iter().next())
936 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb);
937 wgpu_surface.configure(
938 &renderer.device,
939 &wgpu::SurfaceConfiguration {
940 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
941 format,
942 width: self.width,
943 height: self.height,
944 present_mode: wgpu::PresentMode::Fifo,
945 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
946 view_formats: vec![],
947 desired_maximum_frame_latency: 1,
948 },
949 );
950 let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
953 let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
954 self.gpu = Some(GpuState {
955 renderer: renderer.clone(),
956 surface,
957 format,
958 per_output_uniforms: std::sync::Arc::new(renderer.create_per_output_uniforms()),
959 });
960 Ok(())
961 }
962
963 fn set_wallpaper(
964 &mut self,
965 path: &std::path::Path,
966 effect: &crate::animation::Effect,
967 duration_ms: u32,
968 scaling_mode: u32,
969 ) -> anyhow::Result<()> {
970 if !self.blanked
976 && (self.current_bind.is_some() || self.shm_buffers.iter().any(|b| b.is_some()))
977 && self.last_wallpaper.as_deref() == Some(path)
978 && self.scaling_mode == scaling_mode
979 && self.current_effect.as_ref() == Some(effect)
980 {
981 return Ok(());
982 }
983 let is_animated_image = matches!(
988 path.extension().and_then(|extension| extension.to_str()),
989 Some("gif" | "GIF" | "apng" | "APNG")
990 );
991 if duration_ms == 0
992 && !is_animated_image
993 && !crate::video::VideoDecoder::is_video_file(path)
994 && !self.gpu_surface_used
995 && self.commit_static_shm(path, scaling_mode)?
996 {
997 self.playback_gen.fetch_add(1, Ordering::SeqCst);
998 self.pacer.notify();
999 self.video_playback.stop();
1000 self.current_bind = None;
1001 self.current_tex = None;
1002 self.current_width = 0;
1003 self.current_height = 0;
1004 self.scaling_mode = scaling_mode;
1005 self.current_effect = Some(effect.clone());
1006 self.last_wallpaper = Some(path.to_path_buf());
1007 self.blanked = false;
1008 return Ok(());
1014 }
1015 self.ensure_gpu()?;
1016 let commit = self.commit_wallpaper(path, scaling_mode)?;
1017 self.gpu_surface_used = true;
1018 self.scaling_mode = scaling_mode;
1019 self.current_effect = Some(effect.clone());
1020 self.spawn_transition(commit, effect, duration_ms);
1021 self.last_wallpaper = Some(path.to_path_buf());
1023 Ok(())
1024 }
1025
1026 fn commit_static_shm(
1027 &mut self,
1028 path: &std::path::Path,
1029 scaling_mode: u32,
1030 ) -> anyhow::Result<bool> {
1031 use image::{ImageDecoder, ImageReader};
1032
1033 let Some(_render_guard) = self.render_lock.try_lock().ok() else {
1042 return Ok(false);
1043 };
1044
1045 let decoder = ImageReader::open(path)?.into_decoder()?;
1046 let (source_width, source_height) = decoder.dimensions();
1047 Renderer::validate_static_decode(source_width, source_height, decoder.total_bytes())?;
1048 let image = image::DynamicImage::from_decoder(decoder)?;
1049 let (rgba, width, height) = crate::renderer::prepare_image_rgba(
1050 &image,
1051 self.width,
1052 self.height,
1053 scaling_mode,
1054 wgpu::Limits::default().max_texture_dimension_2d,
1055 )?;
1056 if width != self.width || height != self.height {
1057 return Ok(false);
1061 }
1062
1063 let stride = width
1064 .checked_mul(4)
1065 .ok_or_else(|| anyhow::anyhow!("static image stride overflow"))?;
1066 let mut used_slot = 0usize;
1072 let mut chosen: Option<ShmBuffer> = None;
1073 if self.shm_width == width && self.shm_height == height {
1074 for _ in 0..2 {
1075 let slot = self.shm_index % 2;
1076 self.shm_index = self.shm_index.wrapping_add(1);
1077 if let Some(buffer) = self.shm_buffers[slot].take() {
1078 if buffer.canvas(&mut self.shm_pool).is_some() {
1079 chosen = Some(buffer);
1080 used_slot = slot;
1081 break;
1082 }
1083 self.shm_buffers[slot] = Some(buffer);
1084 }
1085 }
1086 if chosen.is_none() {
1087 'wait: for _ in 0..10 {
1090 std::thread::sleep(std::time::Duration::from_millis(5));
1091 for slot in 0..2 {
1092 if let Some(buffer) = self.shm_buffers[slot].take() {
1093 if buffer.canvas(&mut self.shm_pool).is_some() {
1094 chosen = Some(buffer);
1095 used_slot = slot;
1096 break 'wait;
1097 }
1098 self.shm_buffers[slot] = Some(buffer);
1099 }
1100 }
1101 }
1102 }
1103 } else {
1104 self.shm_buffers = [None, None];
1106 }
1107 let buffer = match chosen {
1108 Some(buffer) => buffer,
1109 None => {
1110 if self.shm_width != width || self.shm_height != height {
1115 self.shm_buffers = [None, None];
1117 let (new_buffer, _) = self.shm_pool.create_buffer(
1118 width as i32,
1119 height as i32,
1120 stride as i32,
1121 wayland_client::protocol::wl_shm::Format::Xrgb8888,
1122 )?;
1123 used_slot = 0;
1124 new_buffer
1125 } else if let Some(slot) = self.shm_buffers.iter().position(|slot| slot.is_none()) {
1126 let (new_buffer, _) = self.shm_pool.create_buffer(
1128 width as i32,
1129 height as i32,
1130 stride as i32,
1131 wayland_client::protocol::wl_shm::Format::Xrgb8888,
1132 )?;
1133 used_slot = slot;
1134 new_buffer
1135 } else {
1136 return Ok(false);
1141 }
1142 }
1143 };
1144 let canvas = buffer
1145 .canvas(&mut self.shm_pool)
1146 .ok_or_else(|| anyhow::anyhow!("shared-memory wallpaper buffer is still active"))?;
1147 for (src, dst) in rgba
1148 .as_raw()
1149 .chunks_exact(4)
1150 .zip(canvas.chunks_exact_mut(4))
1151 {
1152 dst[0] = src[2];
1156 dst[1] = src[1];
1157 dst[2] = src[0];
1158 dst[3] = 0xff;
1159 }
1160 buffer
1161 .attach_to(&self.shm_surface)
1162 .map_err(|e| anyhow::anyhow!(e))?;
1163 self.shm_surface
1164 .damage_buffer(0, 0, width as i32, height as i32);
1165 self.shm_surface.commit();
1166 self.shm_buffers[used_slot] = Some(buffer);
1167 self.shm_width = width;
1168 self.shm_height = height;
1169 Ok(true)
1170 }
1171
1172 fn commit_wallpaper(
1176 &mut self,
1177 path: &std::path::Path,
1178 scaling_mode: u32,
1179 ) -> anyhow::Result<CommitData> {
1180 use image::{ImageDecoder, ImageReader};
1181 let gpu = self
1182 .gpu
1183 .as_ref()
1184 .expect("GPU state required for dynamic content");
1185
1186 if crate::video::VideoDecoder::is_video_file(path) {
1188 tracing::info!("Video file detected: {:?}", path);
1189
1190 let generation = self.playback_gen.load(Ordering::SeqCst).wrapping_add(1);
1191 let renderer = gpu.renderer.clone();
1192 let mut prepared = crate::video::VideoPlayback::prepare(
1196 path,
1197 self.hw_accel,
1198 self.preload_frames,
1199 self.loop_video,
1200 std::time::Duration::from_secs(5),
1203 move |metadata| {
1204 renderer
1205 .validate_video_texture(metadata.width, metadata.height)
1206 .map_err(crate::video::VideoError::GpuResourceCreation)
1207 },
1208 )?;
1209 let metadata = prepared.metadata().clone();
1210 let first_frame = prepared.take_first_frame();
1211
1212 let (tex_width, tex_height) = first_frame
1213 .as_ref()
1214 .map(|frame| (frame.width, frame.height))
1215 .unwrap_or((metadata.width, metadata.height));
1216
1217 let video_texture = if let Some(cached) = self.cached_video_texture.as_ref()
1220 && cached.matches_dimensions(tex_width, tex_height)
1221 {
1222 tracing::debug!("Reusing video texture for {}x{}", tex_width, tex_height);
1223 cached.clone()
1225 } else {
1226 if let Some(previous) = self.cached_video_texture.as_ref() {
1227 tracing::debug!(
1228 "Video resolution changed, recreating texture: {}x{} -> {}x{}",
1229 previous.width(),
1230 previous.height(),
1231 tex_width,
1232 tex_height
1233 );
1234 }
1235 let new_texture =
1236 std::sync::Arc::new(gpu.renderer.create_video_texture(tex_width, tex_height)?);
1237 self.cached_video_texture = Some(new_texture.clone());
1239 new_texture
1240 };
1241 let (img_width, img_height) = if let Some(frame) = first_frame {
1242 gpu.renderer
1243 .update_video_texture(&video_texture, &frame.data)?;
1244 (frame.width, frame.height)
1245 } else {
1246 tracing::warn!("No first frame available, using black texture");
1248 (metadata.width, metadata.height)
1249 };
1250 self.video_playback.commit(prepared, generation);
1251 self.playback_gen.store(generation, Ordering::SeqCst);
1252 self.pacer.notify();
1253 let new_tex = video_texture.texture().clone();
1254 let new_bind = video_texture.bind_group().clone();
1255
1256 let old_bind = self.current_bind.take();
1257 let (old_img_width, old_img_height) = if old_bind.is_some() {
1258 (self.current_width.max(1), self.current_height.max(1))
1259 } else {
1260 (img_width, img_height)
1261 };
1262 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
1263
1264 drop(self.current_tex.take());
1265 self.current_tex = Some(new_tex);
1266 self.current_bind = Some(new_bind.clone());
1267 self.current_width = img_width;
1268 self.current_height = img_height;
1269
1270 return Ok(CommitData {
1271 bg_bind,
1272 new_bind,
1273 img_width,
1274 img_height,
1275 old_img_width,
1276 old_img_height,
1277 format: gpu.format,
1278 width: self.width,
1279 height: self.height,
1280 animated: None,
1281 is_video: true,
1282 video_texture: Some(video_texture),
1283 generation,
1284 scaling_mode,
1285 max_fps: self.max_fps,
1286 });
1287 }
1288
1289 let mut animated = crate::animated::AnimatedImage::decode(path)?;
1292 let (new_tex, new_bind, img_width, img_height) = if let Some(anim) = animated.as_mut() {
1293 let (w, h) = (anim.width, anim.height);
1294 let (tex, bind) = gpu.renderer.create_texture(w, h)?;
1295 let first = anim.first_frame();
1296 if !first.is_empty() {
1297 gpu.renderer.update_texture(&tex, first, w, h);
1298 }
1299 (tex, bind, w, h)
1300 } else {
1301 let decoder = ImageReader::open(path)?.into_decoder()?;
1302 let (source_width, source_height) = decoder.dimensions();
1303 Renderer::validate_static_decode(source_width, source_height, decoder.total_bytes())?;
1304 let new_img = image::DynamicImage::from_decoder(decoder)?;
1305 let (tex, bind, width, height) =
1306 gpu.renderer
1307 .load_texture(&new_img, self.width, self.height, scaling_mode)?;
1308 (tex, bind, width, height)
1309 };
1310
1311 self.video_playback.stop();
1314 self.cached_video_texture = None;
1317
1318 let old_bind = self.current_bind.take();
1319 let (old_img_width, old_img_height) = if old_bind.is_some() {
1320 (self.current_width.max(1), self.current_height.max(1))
1321 } else {
1322 (img_width, img_height)
1323 };
1324 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
1329
1330 drop(self.current_tex.take());
1331 self.current_tex = Some(new_tex);
1332 self.current_bind = Some(new_bind.clone());
1333 self.current_width = img_width;
1334 self.current_height = img_height;
1335
1336 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
1337 self.pacer.notify();
1338
1339 Ok(CommitData {
1340 bg_bind,
1341 new_bind,
1342 img_width,
1343 img_height,
1344 old_img_width,
1345 old_img_height,
1346 format: gpu.format,
1347 width: self.width,
1348 height: self.height,
1349 animated,
1350 is_video: false,
1351 video_texture: None,
1352 generation,
1353 scaling_mode,
1354 max_fps: self.max_fps,
1355 })
1356 }
1357
1358 fn spawn_transition(
1363 &self,
1364 commit: CommitData,
1365 effect: &crate::animation::Effect,
1366 duration_ms: u32,
1367 ) {
1368 let gpu = self
1369 .gpu
1370 .as_ref()
1371 .expect("GPU state required for transitions");
1372 let renderer = gpu.renderer.clone();
1373 let surface: &'static wgpu::Surface<'static> = gpu.surface;
1374 let render_lock = self.render_lock.clone();
1375 let playback_gen = self.playback_gen.clone();
1376 let pacer = self.pacer.clone();
1377 let video_playback = self.video_playback.clone();
1378 let per_output_uniforms = std::sync::Arc::clone(&gpu.per_output_uniforms);
1379 let gif_paused = self.gif_paused.clone();
1380 let effect = effect.clone();
1381 drop(tokio::task::spawn_blocking(move || {
1382 render_transition(
1383 renderer,
1384 surface,
1385 render_lock,
1386 playback_gen,
1387 pacer,
1388 video_playback,
1389 gif_paused,
1390 commit,
1391 effect,
1392 duration_ms,
1393 &per_output_uniforms,
1394 );
1395 }));
1396 }
1397}
1398
1399async fn restore_cached_wallpaper(name: &str, render_state: &Arc<Mutex<RenderState>>) {
1400 let state_root = wallpaper_state_root();
1401 let Some(path) = read_wallpaper_state(&state_root, "last_wallpaper", name) else {
1402 return;
1403 };
1404
1405 let effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1406 if let Err(err) = set_wallpaper_with_retry(render_state, &path, &effect, 0, 0).await {
1411 tracing::warn!("Failed to restore wallpaper for {name} from {path:?}: {err}");
1412 let Some(previous) = read_wallpaper_state(&state_root, "previous_wallpaper", name) else {
1413 return;
1414 };
1415 match set_wallpaper_with_retry(render_state, &previous, &effect, 0, 0).await {
1416 Ok(()) => {
1417 if let Err(persist_err) =
1418 write_wallpaper_state(&state_root, "last_wallpaper", name, &previous)
1419 {
1420 tracing::warn!(
1421 "Restored previous wallpaper for {name}, but failed to update state: {persist_err}"
1422 );
1423 } else {
1424 tracing::warn!("Restored previous wallpaper for {name} after {path:?} failed");
1425 }
1426 }
1427 Err(previous_err) => tracing::warn!(
1428 "Failed to restore previous wallpaper for {name} from {previous:?}: {previous_err}"
1429 ),
1430 }
1431 }
1432}
1433
1434const WALLPAPER_RETRY_ATTEMPTS: usize = 3;
1435const WALLPAPER_RETRY_DELAY: std::time::Duration = std::time::Duration::from_millis(150);
1436const INITIAL_SHM_POOL_BYTES: usize = 4096;
1440
1441async fn set_wallpaper_with_retry(
1442 render_state: &Arc<Mutex<RenderState>>,
1443 path: &std::path::Path,
1444 effect: &crate::animation::Effect,
1445 duration_ms: u32,
1446 scaling_mode: u32,
1447) -> anyhow::Result<()> {
1448 let render_state = Arc::clone(render_state);
1449 let path = path.to_path_buf();
1450 let effect = effect.clone();
1451
1452 tokio::task::spawn_blocking(move || {
1453 let mut attempt = 1;
1454 loop {
1455 let result = {
1456 let mut state = render_state.blocking_lock();
1457 state.set_wallpaper(&path, &effect, duration_ms, scaling_mode)
1458 };
1459 match result {
1460 Ok(()) => return Ok(()),
1461 Err(err)
1462 if attempt < WALLPAPER_RETRY_ATTEMPTS
1463 && is_transient_wallpaper_error(&err) =>
1464 {
1465 tracing::warn!(
1466 "Transient wallpaper error for {path:?} (attempt {attempt}/{WALLPAPER_RETRY_ATTEMPTS}): {err}"
1467 );
1468 attempt += 1;
1469 std::thread::sleep(WALLPAPER_RETRY_DELAY);
1470 }
1471 Err(err) => return Err(err),
1472 }
1473 }
1474 })
1475 .await?
1476}
1477
1478fn is_transient_wallpaper_error(err: &anyhow::Error) -> bool {
1479 err.chain().any(|cause| {
1480 cause
1481 .downcast_ref::<std::io::Error>()
1482 .is_some_and(|io_err| {
1483 matches!(
1484 io_err.kind(),
1485 std::io::ErrorKind::Interrupted
1486 | std::io::ErrorKind::WouldBlock
1487 | std::io::ErrorKind::TimedOut
1488 )
1489 })
1490 || cause
1491 .downcast_ref::<crate::video::VideoError>()
1492 .is_some_and(crate::video::VideoError::is_recoverable)
1493 })
1494}
1495
1496fn wallpaper_state_root() -> std::path::PathBuf {
1497 dirs::cache_dir()
1498 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1499 .join("wallr")
1500}
1501
1502fn wallpaper_state_path(root: &std::path::Path, state_dir: &str, name: &str) -> std::path::PathBuf {
1503 root.join(state_dir).join(name)
1504}
1505
1506fn read_wallpaper_state(
1507 root: &std::path::Path,
1508 state_dir: &str,
1509 name: &str,
1510) -> Option<std::path::PathBuf> {
1511 use std::os::unix::ffi::OsStringExt;
1512
1513 let path = std::fs::read(wallpaper_state_path(root, state_dir, name)).ok()?;
1514 let wallpaper = std::path::PathBuf::from(std::ffi::OsString::from_vec(path));
1515 (!wallpaper.as_os_str().is_empty()).then_some(wallpaper)
1516}
1517
1518fn write_wallpaper_state(
1519 root: &std::path::Path,
1520 state_dir: &str,
1521 name: &str,
1522 wallpaper: &std::path::Path,
1523) -> std::io::Result<()> {
1524 use std::io::Write;
1525 use std::os::unix::ffi::OsStrExt;
1526 use std::sync::atomic::{AtomicU64, Ordering};
1527
1528 static TEMP_ID: AtomicU64 = AtomicU64::new(0);
1529
1530 let state_path = wallpaper_state_path(root, state_dir, name);
1531 let parent = state_path
1532 .parent()
1533 .ok_or_else(|| std::io::Error::other("wallpaper state path has no parent"))?;
1534 std::fs::create_dir_all(parent)?;
1535 let file_name = state_path
1536 .file_name()
1537 .and_then(|value| value.to_str())
1538 .unwrap_or("wallpaper");
1539
1540 loop {
1541 let id = TEMP_ID.fetch_add(1, Ordering::Relaxed);
1542 let temporary_path = parent.join(format!(".{file_name}.{}.{}.tmp", std::process::id(), id));
1543 match std::fs::OpenOptions::new()
1544 .write(true)
1545 .create_new(true)
1546 .open(&temporary_path)
1547 {
1548 Ok(mut temporary) => {
1549 if let Err(err) = temporary
1550 .write_all(wallpaper.as_os_str().as_bytes())
1551 .and_then(|()| temporary.sync_all())
1552 .and_then(|()| std::fs::rename(&temporary_path, &state_path))
1553 {
1554 let _ = std::fs::remove_file(&temporary_path);
1555 return Err(err);
1556 }
1557 return Ok(());
1558 }
1559 Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
1560 Err(err) => return Err(err),
1561 }
1562 }
1563}
1564
1565fn persist_wallpaper(name: &str, wallpaper: &std::path::Path) -> std::io::Result<()> {
1566 let root = wallpaper_state_root();
1567 persist_wallpaper_at(&root, name, wallpaper)
1568}
1569
1570fn persist_wallpaper_at(
1571 root: &std::path::Path,
1572 name: &str,
1573 wallpaper: &std::path::Path,
1574) -> std::io::Result<()> {
1575 let current = read_wallpaper_state(root, "last_wallpaper", name);
1576 if current.as_deref() == Some(wallpaper) {
1581 return Ok(());
1582 }
1583 let previous = current.filter(|current| current.exists() && current != wallpaper);
1584 write_wallpaper_state(root, "last_wallpaper", name, wallpaper)?;
1585 if let Some(previous) = previous {
1586 write_wallpaper_state(root, "previous_wallpaper", name, &previous)?;
1587 }
1588 Ok(())
1589}
1590
1591#[allow(clippy::too_many_arguments)]
1599fn render_transition(
1600 renderer: std::sync::Arc<Renderer>,
1601 surface: &'static wgpu::Surface<'static>,
1602 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
1603 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
1604 pacer: std::sync::Arc<LivePacer>,
1605 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
1606 gif_paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
1607 mut commit: CommitData,
1608 effect: crate::animation::Effect,
1609 duration_ms: u32,
1610 per_output_uniforms: &crate::renderer::PerOutputUniforms,
1611) {
1612 if playback_gen.load(Ordering::Acquire) != commit.generation {
1616 reclaim_commit_resources(&renderer, commit);
1617 return;
1618 }
1619 let _guard = render_lock
1620 .lock()
1621 .unwrap_or_else(|poisoned| poisoned.into_inner());
1622
1623 let duration = std::time::Duration::from_millis(u64::from(duration_ms.max(1)));
1624 let start = std::time::Instant::now();
1625 loop {
1626 if playback_gen.load(Ordering::Acquire) != commit.generation {
1631 reclaim_commit_resources(&renderer, commit);
1632 return;
1633 }
1634 let progress = start.elapsed().as_secs_f32() / duration.as_secs_f32();
1635 let uniforms = crate::animation::compute_effect_uniforms(&effect, progress.clamp(0.0, 1.0));
1636 let status = renderer.render_frame(
1637 crate::renderer::FrameRequest {
1638 surface,
1639 format: commit.format,
1640 bg_bind: &commit.bg_bind,
1641 new_bind: &commit.new_bind,
1642 effect: &uniforms,
1643 width: commit.width,
1644 height: commit.height,
1645 img_width: commit.img_width,
1646 img_height: commit.img_height,
1647 old_img_width: commit.old_img_width,
1648 old_img_height: commit.old_img_height,
1649 scaling_mode: commit.scaling_mode,
1650 },
1651 per_output_uniforms,
1652 );
1653 let status = match status {
1654 Ok(status) => status,
1655 Err(err) => {
1656 eprintln!("wallr: transition render failed: {err}");
1657 break;
1658 }
1659 };
1660 if progress >= 1.0 {
1661 break;
1662 }
1663 match status {
1664 crate::renderer::FrameStatus::Presented => {}
1665 crate::renderer::FrameStatus::TimedOut => {
1670 std::thread::sleep(std::time::Duration::from_millis(4));
1671 }
1672 crate::renderer::FrameStatus::Outdated | crate::renderer::FrameStatus::Lost => {
1675 surface.configure(
1676 &renderer.device,
1677 &wgpu::SurfaceConfiguration {
1678 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1679 format: commit.format,
1680 width: commit.width.max(1),
1681 height: commit.height.max(1),
1682 present_mode: wgpu::PresentMode::Fifo,
1683 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
1684 view_formats: vec![],
1685 desired_maximum_frame_latency: 1,
1686 },
1687 );
1688 }
1689 }
1690 }
1691
1692 let mut animated = commit.animated.take();
1696 if let Some(animated) = animated.as_mut()
1697 && playback_gen.load(Ordering::SeqCst) == commit.generation
1698 {
1699 play_live(
1700 &renderer,
1701 surface,
1702 &commit,
1703 animated,
1704 &playback_gen,
1705 &pacer,
1706 &gif_paused,
1707 per_output_uniforms,
1708 );
1709 } else if commit.is_video && playback_gen.load(Ordering::SeqCst) == commit.generation {
1710 play_video(
1711 &renderer,
1712 surface,
1713 &commit,
1714 &video_playback,
1715 &playback_gen,
1716 &pacer,
1717 per_output_uniforms,
1718 );
1719 }
1720 reclaim_commit_resources(&renderer, commit);
1724}
1725
1726fn reclaim_commit_resources(renderer: &Renderer, commit: CommitData) {
1727 drop(commit);
1728 renderer.device.poll(wgpu::Maintain::Wait);
1729}
1730
1731fn advance_gif_slots(
1739 cur: usize,
1740 cur_frame: usize,
1741 next_frame: usize,
1742 index: usize,
1743 upload_ok: bool,
1744) -> (usize, usize, usize, bool) {
1745 if index == cur_frame {
1746 return (cur, cur_frame, next_frame, true);
1747 }
1748 let target = cur ^ 1;
1749 if next_frame == index {
1750 return (target, index, next_frame, true);
1752 }
1753 if upload_ok {
1754 return (target, index, index, true);
1755 }
1756 (cur, cur_frame, next_frame, false)
1757}
1758
1759#[allow(clippy::too_many_arguments)]
1765fn play_live(
1766 renderer: &Renderer,
1767 surface: &'static wgpu::Surface<'static>,
1768 commit: &CommitData,
1769 animated: &mut crate::animated::AnimatedImage,
1770 playback_gen: &std::sync::atomic::AtomicU64,
1771 pacer: &LivePacer,
1772 gif_paused: &std::sync::Arc<std::sync::atomic::AtomicBool>,
1773 per_output_uniforms: &crate::renderer::PerOutputUniforms,
1774) {
1775 let Ok((tex_a, bind_a)) = renderer.create_texture(animated.width, animated.height) else {
1776 tracing::warn!(
1777 "GIF dimensions {}x{} exceed the GPU texture limit",
1778 animated.width,
1779 animated.height
1780 );
1781 return;
1782 };
1783 let Ok((tex_b, bind_b)) = renderer.create_texture(animated.width, animated.height) else {
1784 tracing::warn!(
1785 "GIF dimensions {}x{} exceed the GPU texture limit",
1786 animated.width,
1787 animated.height
1788 );
1789 return;
1790 };
1791 let (frame_w, frame_h) = (animated.width, animated.height);
1792 let (bytes_per_row, rows) = (frame_w * 4, frame_h);
1793 let frame_bytes = bytes_per_row as u64 * rows as u64;
1794
1795 let direct_upload = bytes_per_row % 256 == 0;
1798 let staging: Vec<wgpu::Buffer> = if direct_upload {
1799 (0..2)
1800 .map(|_| {
1801 renderer.device.create_buffer(&wgpu::BufferDescriptor {
1802 label: Some("wallr-gif-staging"),
1803 size: frame_bytes,
1804 usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
1805 mapped_at_creation: false,
1806 })
1807 })
1808 .collect()
1809 } else {
1810 Vec::new()
1811 };
1812
1813 let first = animated.first_frame();
1814 if !first.is_empty() {
1815 renderer.update_texture(&tex_a, first, frame_w, frame_h);
1816 renderer.update_texture(&tex_b, first, frame_w, frame_h);
1817 }
1818 let binds = [bind_a, bind_b];
1819 let textures = [tex_a, tex_b];
1820
1821 let upload = |renderer: &Renderer,
1824 tgt: usize,
1825 index: usize,
1826 slot: usize,
1827 animated: &mut crate::animated::AnimatedImage|
1828 -> bool {
1829 if direct_upload {
1830 let buffer = &staging[slot];
1831 let slice = buffer.slice(..);
1832 slice.map_async(wgpu::MapMode::Write, |_| {});
1833 renderer.device.poll(wgpu::Maintain::Wait);
1834 let ok = {
1835 let mut mapped = slice.get_mapped_range_mut();
1836 animated.decompress_into(index, &mut mapped)
1837 };
1838 buffer.unmap();
1839 if ok {
1840 let mut encoder = renderer
1841 .device
1842 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
1843 encoder.copy_buffer_to_texture(
1844 wgpu::TexelCopyBufferInfo {
1845 buffer,
1846 layout: wgpu::TexelCopyBufferLayout {
1847 offset: 0,
1848 bytes_per_row: Some(bytes_per_row),
1849 rows_per_image: Some(rows),
1850 },
1851 },
1852 wgpu::TexelCopyTextureInfo {
1853 texture: &textures[tgt],
1854 mip_level: 0,
1855 origin: wgpu::Origin3d::ZERO,
1856 aspect: wgpu::TextureAspect::All,
1857 },
1858 wgpu::Extent3d {
1859 width: frame_w,
1860 height: frame_h,
1861 depth_or_array_layers: 1,
1862 },
1863 );
1864 renderer.queue.submit([encoder.finish()]);
1865 return true;
1866 }
1867 } else {
1868 let frame = animated.frame_at(index);
1869 if !frame.is_empty() {
1870 renderer.update_texture(&textures[tgt], frame, frame_w, frame_h);
1871 return true;
1872 }
1873 }
1874 false
1875 };
1876
1877 let mut cur = 0usize; let mut cur_frame = 0usize; let mut next_frame = 0usize; let mut slot = 0usize; let start = std::time::Instant::now();
1882 let mut paused_elapsed = std::time::Duration::ZERO; let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1884 loop {
1885 if playback_gen.load(Ordering::SeqCst) != commit.generation {
1886 return;
1887 }
1888
1889 if gif_paused.load(Ordering::SeqCst) {
1891 let pause_start = std::time::Instant::now();
1896 pacer.wait_until(std::time::Instant::now() + std::time::Duration::from_secs(86_400));
1897 paused_elapsed += pause_start.elapsed();
1898 continue;
1899 }
1900
1901 let index = animated.frame_index_at(start.elapsed() - paused_elapsed);
1902 if index != cur_frame {
1903 let target = cur ^ 1;
1912 let upload_ok = if next_frame == index {
1913 true
1914 } else {
1915 let ok = upload(renderer, target, index, slot, animated);
1916 if ok {
1917 slot ^= 1;
1918 }
1919 ok
1920 };
1921 let (new_cur, new_cur_frame, new_next, displayable) =
1922 advance_gif_slots(cur, cur_frame, next_frame, index, upload_ok);
1923 if !displayable {
1924 pacer.wait_until(std::time::Instant::now() + std::time::Duration::from_millis(2));
1929 continue;
1930 }
1931 cur = new_cur;
1932 cur_frame = new_cur_frame;
1933 next_frame = new_next;
1934 }
1935 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
1936 let status = renderer.render_frame(
1937 crate::renderer::FrameRequest {
1938 surface,
1939 format: commit.format,
1940 bg_bind: &binds[cur],
1941 new_bind: &binds[cur],
1942 effect: &uniforms,
1943 width: commit.width,
1944 height: commit.height,
1945 img_width: animated.width,
1946 img_height: animated.height,
1947 old_img_width: animated.width,
1948 old_img_height: animated.height,
1949 scaling_mode: commit.scaling_mode,
1950 },
1951 per_output_uniforms,
1952 );
1953 match status {
1954 Ok(crate::renderer::FrameStatus::Presented) => {}
1955 Ok(crate::renderer::FrameStatus::TimedOut) => {
1959 std::thread::sleep(std::time::Duration::from_millis(4));
1960 }
1961 Ok(crate::renderer::FrameStatus::Outdated | crate::renderer::FrameStatus::Lost) => {
1962 surface.configure(
1963 &renderer.device,
1964 &wgpu::SurfaceConfiguration {
1965 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1966 format: commit.format,
1967 width: commit.width.max(1),
1968 height: commit.height.max(1),
1969 present_mode: wgpu::PresentMode::Fifo,
1970 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
1971 view_formats: vec![],
1972 desired_maximum_frame_latency: 1,
1973 },
1974 );
1975 }
1976 Err(err) => {
1977 tracing::warn!("GIF present failed ({err}), stopping playback");
1978 return;
1979 }
1980 }
1981
1982 let elapsed = start.elapsed() - paused_elapsed;
1990 let total: std::time::Duration = animated.total_duration();
1991 let loops = (elapsed.as_millis() / total.as_millis().max(1)) as u64;
1992 let next_change = animated.frame_start(index + 1) + total * (loops as u32);
1993 let wait = next_change.saturating_sub(elapsed);
1994 if wait > std::time::Duration::ZERO {
1995 let next = index + 1;
1996 if next_frame != next && upload(renderer, cur ^ 1, next, slot, animated) {
1997 slot ^= 1;
1998 next_frame = next;
1999 }
2000 pacer.wait_until(std::time::Instant::now() + wait);
2003 }
2004 }
2005}
2006
2007#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2008enum VideoPresentAction {
2009 Presented,
2010 Retry,
2011 Reconfigure,
2012}
2013
2014fn video_present_action(status: crate::renderer::FrameStatus) -> VideoPresentAction {
2015 match status {
2016 crate::renderer::FrameStatus::Presented => VideoPresentAction::Presented,
2017 crate::renderer::FrameStatus::TimedOut => VideoPresentAction::Retry,
2018 crate::renderer::FrameStatus::Outdated | crate::renderer::FrameStatus::Lost => {
2019 VideoPresentAction::Reconfigure
2020 }
2021 }
2022}
2023
2024fn play_video(
2026 renderer: &Renderer,
2027 surface: &'static wgpu::Surface<'static>,
2028 commit: &CommitData,
2029 video_playback: &std::sync::Arc<crate::video::VideoPlayback>,
2030 playback_gen: &std::sync::atomic::AtomicU64,
2031 pacer: &LivePacer,
2032 per_output_uniforms: &crate::renderer::PerOutputUniforms,
2033) {
2034 let mut width = commit.img_width;
2035 let mut height = commit.img_height;
2036
2037 let Some(initial) = commit.video_texture.as_ref() else {
2038 tracing::warn!("Video conversion resources unavailable");
2039 return;
2040 };
2041 let mut owned_replacement: Option<crate::renderer::VideoTexture> = None;
2045 let mut texture: &crate::renderer::VideoTexture = initial.as_ref();
2046 let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
2047
2048 let min_frame_interval = commit
2049 .max_fps
2050 .filter(|fps| *fps > 0)
2051 .map(|fps| std::time::Duration::from_secs_f64(1.0 / f64::from(fps)));
2052 let mut last_present: Option<std::time::Instant> = None;
2053 let mut last_frame_at = std::time::Instant::now();
2056
2057 loop {
2058 if playback_gen.load(Ordering::SeqCst) != commit.generation {
2063 return;
2064 }
2065
2066 if let (Some(interval), Some(previous)) = (min_frame_interval, last_present) {
2067 pacer.wait_until(previous + interval);
2068 if playback_gen.load(Ordering::SeqCst) != commit.generation {
2069 return;
2070 }
2071 }
2072
2073 let mut frame_uploaded = false;
2076 if let Some(frame) = video_playback.next_frame_in_generation(commit.generation) {
2077 if frame.width != width || frame.height != height {
2080 match renderer.create_video_texture(frame.width, frame.height) {
2081 Ok(replacement) => {
2082 tracing::info!(
2083 "Video resolution changed {}x{} -> {}x{}, recreated conversion resources",
2084 width,
2085 height,
2086 frame.width,
2087 frame.height
2088 );
2089 width = frame.width;
2090 height = frame.height;
2091 owned_replacement.take();
2095 owned_replacement = Some(replacement);
2096 texture = owned_replacement.as_ref().expect("just stored");
2097 }
2098 Err(err) => {
2099 tracing::warn!("Video resolution change rejected: {err}");
2100 pacer.wait_until(
2101 std::time::Instant::now() + std::time::Duration::from_millis(2),
2102 );
2103 continue;
2104 }
2105 }
2106 }
2107 if let Err(err) = renderer.update_video_texture(texture, &frame.data) {
2108 tracing::warn!("Video frame upload failed: {err}");
2111 } else {
2112 last_frame_at = std::time::Instant::now();
2113 frame_uploaded = true;
2114 }
2115 }
2118
2119 if !frame_uploaded {
2120 if playback_gen.load(Ordering::SeqCst) != commit.generation {
2121 return;
2122 }
2123 if video_playback.is_paused() {
2124 pacer.wait_until(std::time::Instant::now() + std::time::Duration::from_millis(100));
2126 continue;
2127 }
2128 if last_frame_at.elapsed() > std::time::Duration::from_secs(5) {
2129 tracing::warn!("Video playback stalled, stopping");
2130 return;
2131 }
2132 let wait = video_playback
2133 .time_until_next_frame_in_generation(commit.generation)
2134 .unwrap_or(std::time::Duration::from_millis(2));
2135 if playback_gen.load(Ordering::SeqCst) != commit.generation {
2136 return;
2137 }
2138 pacer.wait_until(std::time::Instant::now() + wait);
2139 continue;
2140 }
2141
2142 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
2143 let status = renderer.render_frame(
2144 crate::renderer::FrameRequest {
2145 surface,
2146 format: commit.format,
2147 bg_bind: texture.bind_group(),
2148 new_bind: texture.bind_group(),
2149 effect: &uniforms,
2150 width: commit.width,
2151 height: commit.height,
2152 img_width: width,
2153 img_height: height,
2154 old_img_width: width,
2155 old_img_height: height,
2156 scaling_mode: commit.scaling_mode,
2157 },
2158 per_output_uniforms,
2159 );
2160
2161 match status.map(video_present_action) {
2162 Ok(VideoPresentAction::Presented) => {
2163 last_present = Some(std::time::Instant::now());
2164 }
2165 Ok(action) => {
2166 if action == VideoPresentAction::Reconfigure {
2167 surface.configure(
2168 &renderer.device,
2169 &wgpu::SurfaceConfiguration {
2170 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2171 format: commit.format,
2172 width: commit.width,
2173 height: commit.height,
2174 present_mode: wgpu::PresentMode::Fifo,
2175 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
2176 view_formats: vec![],
2177 desired_maximum_frame_latency: 1,
2178 },
2179 );
2180 }
2181 pacer.wait_until(std::time::Instant::now() + std::time::Duration::from_millis(100));
2182 }
2183 Err(err) => {
2184 tracing::warn!("Video present failed ({err}), stopping playback");
2185 video_playback.stop();
2186 return;
2187 }
2188 }
2189 }
2190}
2191
2192async fn resolve_targets(
2197 render_states: &std::collections::HashMap<
2198 String,
2199 std::sync::Arc<tokio::sync::Mutex<RenderState>>,
2200 >,
2201 monitor: Option<&str>,
2202) -> Vec<std::sync::Arc<tokio::sync::Mutex<RenderState>>> {
2203 match monitor {
2204 Some(name) => {
2205 if let Some(rs) = render_states.get(name) {
2206 vec![rs.clone()]
2207 } else {
2208 Vec::new()
2209 }
2210 }
2211 None => render_states.values().cloned().collect(),
2212 }
2213}
2214
2215fn resolve_named_targets(
2216 render_states: &std::collections::HashMap<
2217 String,
2218 std::sync::Arc<tokio::sync::Mutex<RenderState>>,
2219 >,
2220 monitor: Option<&str>,
2221) -> Vec<(String, std::sync::Arc<tokio::sync::Mutex<RenderState>>)> {
2222 match monitor {
2223 Some(name) => render_states
2224 .get(name)
2225 .map(|state| vec![(name.to_string(), state.clone())])
2226 .unwrap_or_default(),
2227 None => render_states
2228 .iter()
2229 .map(|(name, state)| (name.clone(), state.clone()))
2230 .collect(),
2231 }
2232}
2233
2234type RenderStateMap = std::collections::HashMap<String, Arc<Mutex<RenderState>>>;
2235type SharedRenderStates = std::sync::Arc<tokio::sync::Mutex<RenderStateMap>>;
2236
2237async fn snapshot_targets(
2241 map: &SharedRenderStates,
2242 monitor: Option<&str>,
2243) -> Vec<Arc<Mutex<RenderState>>> {
2244 let states = map.lock().await;
2245 resolve_targets(&states, monitor).await
2246}
2247
2248async fn snapshot_named_targets(
2249 map: &SharedRenderStates,
2250 monitor: Option<&str>,
2251) -> Vec<(String, Arc<Mutex<RenderState>>)> {
2252 let states = map.lock().await;
2253 resolve_named_targets(&states, monitor)
2254}
2255
2256async fn snapshot_all_named(map: &SharedRenderStates) -> Vec<(String, Arc<Mutex<RenderState>>)> {
2257 let states = map.lock().await;
2258 states
2259 .iter()
2260 .map(|(name, state)| (name.clone(), state.clone()))
2261 .collect()
2262}
2263
2264async fn reap_idle_shm_pools(states: SharedRenderStates) {
2274 loop {
2275 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
2279 for (_, state) in snapshot_all_named(&states).await {
2280 let Ok(mut locked) = state.try_lock() else {
2281 continue;
2282 };
2283 if locked.render_lock.try_lock().is_err() {
2284 continue;
2285 }
2286 if locked.shm_pool.len() > INITIAL_SHM_POOL_BYTES {
2287 locked.shm_buffers = [None, None];
2288 locked.shm_width = 0;
2289 locked.shm_height = 0;
2290 let fresh = SlotPool::new(
2291 INITIAL_SHM_POOL_BYTES,
2292 &smithay_client_toolkit::shm::Shm::from(locked.shm_server.clone()),
2293 );
2294 match fresh {
2295 Ok(pool) => locked.shm_pool = pool,
2296 Err(_) => continue,
2297 }
2298 }
2299 unsafe {
2305 libc::malloc_trim(0);
2306 }
2307 }
2308 }
2309}
2310
2311pub(crate) fn clamp_preload_frames(requested: usize) -> usize {
2316 requested.clamp(1, 3)
2317}
2318
2319pub(crate) fn clamp_max_fps(requested: Option<u32>) -> Option<u32> {
2320 match requested {
2321 Some(0) | None => None,
2322 Some(fps) => Some(fps.clamp(1, 240)),
2323 }
2324}
2325
2326fn is_usable_wallpaper_file(path: &std::path::Path) -> bool {
2330 std::fs::metadata(path).is_ok_and(|m| m.is_file() && m.len() > 0)
2331}
2332
2333fn validate_live_config(cfg: &WallrConfig) -> Result<(), String> {
2337 if cfg.video.preload_frames > 64 {
2338 return Err(format!(
2339 "video.preload_frames {} exceeds maximum 64",
2340 cfg.video.preload_frames
2341 ));
2342 }
2343 if let Some(fps) = cfg.daemon.max_fps
2344 && (fps == 0 || fps > 1000)
2345 {
2346 return Err(format!("daemon.max_fps {fps} out of range 1..=1000"));
2347 }
2348 Ok(())
2349}
2350
2351#[cfg(target_os = "linux")]
2361fn tune_allocator() {
2362 unsafe {
2366 libc::mallopt(libc::M_MMAP_THRESHOLD, 128 * 1024);
2367 libc::mallopt(libc::M_TRIM_THRESHOLD, 64 * 1024);
2368 }
2369}
2370
2371pub struct Daemon {
2372 config: WallrConfig,
2373 paused: Arc<AtomicBool>,
2374 engine: Arc<Mutex<WallpaperEngine>>,
2375}
2376
2377impl Daemon {
2378 pub fn new(config: WallrConfig) -> Result<Self, DaemonError> {
2379 let engine = WallpaperEngine::new(config.clone())?;
2380 Ok(Self {
2381 config,
2382 paused: Arc::new(AtomicBool::new(false)),
2383 engine: Arc::new(Mutex::new(engine)),
2384 })
2385 }
2386
2387 pub async fn start(self) -> Result<(), DaemonError> {
2388 tune_allocator();
2389 let socket_path = crate::config::expand_path(&self.config.daemon.socket);
2390 if socket_path.exists() {
2391 if tokio::net::UnixStream::connect(&socket_path).await.is_ok() {
2392 return Err(DaemonError::AlreadyRunning(
2393 socket_path.to_string_lossy().to_string(),
2394 ));
2395 }
2396 let _ = std::fs::remove_file(&socket_path);
2397 }
2398
2399 let conn = Connection::connect_to_env()
2400 .map_err(|e| DaemonError::StartError(format!("Failed to connect to Wayland: {e:?}")))?;
2401 let backend = conn.backend();
2402 let display_ptr = backend.display_ptr() as *mut std::ffi::c_void;
2403
2404 let (globals, mut event_queue) = registry_queue_init(&conn)
2405 .map_err(|e| DaemonError::StartError(format!("registry_queue_init failed: {e:?}")))?;
2406 let qh = event_queue.handle();
2407
2408 let compositor_state = CompositorState::bind(&globals, &qh)
2409 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
2410 let layer_shell = LayerShell::bind(&globals, &qh)
2411 .map_err(|e| DaemonError::StartError(format!("layer_shell bind failed: {e:?}")))?;
2412 let shm = Shm::bind(&globals, &qh)
2413 .map_err(|e| DaemonError::StartError(format!("shm bind failed: {e:?}")))?;
2414
2415 let compositor = globals
2417 .bind::<wl_compositor::WlCompositor, WaylandState, smithay_client_toolkit::globals::GlobalData>(
2418 &qh,
2419 1..=4,
2420 smithay_client_toolkit::globals::GlobalData,
2421 )
2422 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
2423 let viewporter = globals
2424 .bind::<WpViewporter, WaylandState, ()>(&qh, 1..=1, ())
2425 .ok();
2426 if viewporter.is_none() {
2427 tracing::warn!(
2428 "wp_viewporter is unavailable; fractional outputs will use integer buffer scaling"
2429 );
2430 }
2431
2432 let mut wayland_state = WaylandState {
2433 registry_state: RegistryState::new(&globals),
2434 output_state: OutputState::new(&globals, &qh),
2435 compositor_state,
2436 shm,
2437 outputs: std::collections::HashMap::new(),
2438 surfaces: Vec::new(),
2439 viewporter,
2440 viewports: std::collections::HashMap::new(),
2441 output_lifecycles: std::collections::HashMap::new(),
2442 pending_restores: std::collections::HashSet::new(),
2443 layer_shell,
2444 compositor,
2445 hotplug: None,
2446 };
2447
2448 for i in 0..5 {
2452 event_queue
2453 .roundtrip(&mut wayland_state)
2454 .map_err(|e| DaemonError::StartError(format!("roundtrip {i} failed: {e:?}")))?;
2455 }
2456
2457 if wayland_state.outputs.is_empty() {
2458 return Err(DaemonError::StartError(
2459 "no outputs detected after roundtrip".into(),
2460 ));
2461 }
2462
2463 tracing::info!(
2464 "Detected {} output(s): {:?}",
2465 wayland_state.outputs.len(),
2466 wayland_state
2467 .outputs
2468 .values()
2469 .map(|o| format!("{} ({}x{})", o.name, o.width, o.height))
2470 .collect::<Vec<_>>()
2471 );
2472
2473 let renderer: std::sync::Arc<std::sync::Mutex<Option<std::sync::Arc<Renderer>>>> =
2474 std::sync::Arc::new(std::sync::Mutex::new(None));
2475
2476 let mut render_states_map: std::collections::HashMap<String, Arc<Mutex<RenderState>>> =
2480 std::collections::HashMap::new();
2481
2482 let output_info: Vec<(u32, OutputInfo)> = wayland_state
2485 .outputs
2486 .iter()
2487 .map(|(k, v)| {
2488 (
2489 *k,
2490 OutputInfo {
2491 name: v.name.clone(),
2492 width: v.width,
2493 height: v.height,
2494 scale_factor: v.scale_factor,
2495 wl_output: v.wl_output.clone(),
2496 },
2497 )
2498 })
2499 .collect();
2500
2501 for (proto_id, info) in &output_info {
2502 let name = info.name.clone();
2503 let rs = Self::create_render_state_for_output(
2504 &renderer,
2505 display_ptr,
2506 &mut wayland_state,
2507 &qh,
2508 info,
2509 &self.config,
2510 )
2511 .await?;
2512 let rs = Arc::new(Mutex::new(rs));
2513 render_states_map.insert(name.clone(), rs.clone());
2514 wayland_state.output_lifecycles.insert(
2515 *proto_id,
2516 OutputLifecycle {
2517 name: name.clone(),
2518 render_state: rs,
2519 active: Arc::new(std::sync::atomic::AtomicBool::new(true)),
2520 },
2521 );
2522 wayland_state.pending_restores.insert(*proto_id);
2523
2524 tracing::info!("Output ready: {name} ({proto_id})");
2525 }
2526
2527 let render_states: std::sync::Arc<
2530 tokio::sync::Mutex<std::collections::HashMap<String, Arc<Mutex<RenderState>>>>,
2531 > = std::sync::Arc::new(tokio::sync::Mutex::new(render_states_map));
2532
2533 wayland_state.hotplug = Some(DaemonHotplug {
2536 renderer: renderer.clone(),
2537 config: self.config.clone(),
2538 display_ptr: SendDisplayPtr(display_ptr),
2539 render_states: render_states.clone(),
2540 });
2541
2542 event_queue
2545 .roundtrip(&mut wayland_state)
2546 .map_err(|e| DaemonError::StartError(format!("hotplug roundtrip failed: {e:?}")))?;
2547
2548 let paused_clone = self.paused.clone();
2549 let engine_clone = self.engine.clone();
2550 let render_states_clone = render_states.clone();
2551 tokio::spawn(reap_idle_shm_pools(render_states.clone()));
2553 let theme_gen = Arc::new(std::sync::atomic::AtomicU64::new(0));
2557 let theme_gen_clone = theme_gen.clone();
2558
2559 {
2563 let rs_map = render_states.clone();
2564 let socket_path = socket_path.clone();
2565 tokio::spawn(async move {
2566 use tokio::signal::unix::{SignalKind, signal};
2567 let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler");
2568 let mut int = signal(SignalKind::interrupt()).expect("SIGINT handler");
2569 let mut hup = signal(SignalKind::hangup()).expect("SIGHUP handler");
2570 tokio::select! {
2571 _ = term.recv() => {}
2572 _ = int.recv() => {}
2573 _ = hup.recv() => {}
2574 }
2575 tracing::info!("Signal received, shutting down gracefully");
2576 let states = rs_map.lock().await;
2577 for rs in states.values() {
2578 if let Ok(state) = rs.try_lock() {
2579 state.video_playback.stop();
2580 }
2581 }
2582 drop(states);
2583 let _ = std::fs::remove_file(&socket_path);
2584 std::process::exit(0);
2585 });
2586 }
2587
2588 let ipc_socket_path = socket_path.clone();
2589 start_ipc_server(&socket_path, move |cmd| {
2590 let paused = paused_clone.clone();
2591 let engine = engine_clone.clone();
2592 let render_states_map = render_states_clone.clone();
2593 let theme_gen = theme_gen_clone.clone();
2594 let stop_socket = ipc_socket_path.clone();
2595 async move {
2596 match cmd {
2597 IpcCommand::Pause { monitor } => {
2598 let targets =
2599 snapshot_targets(&render_states_map, monitor.as_deref()).await;
2600 if targets.is_empty() {
2601 return IpcResponse {
2602 success: false,
2603 message: Some("No matching outputs".into()),
2604 };
2605 }
2606 if monitor.is_none() {
2607 paused.store(true, Ordering::SeqCst);
2608 }
2609 for rs in targets {
2610 let rs_lock = rs.lock().await;
2611 rs_lock.video_playback.pause();
2612 rs_lock.gif_paused.store(true, Ordering::SeqCst);
2613 }
2614 IpcResponse {
2615 success: true,
2616 message: Some("Paused".into()),
2617 }
2618 }
2619 IpcCommand::Resume { monitor } => {
2620 let targets =
2621 snapshot_targets(&render_states_map, monitor.as_deref()).await;
2622 if targets.is_empty() {
2623 return IpcResponse {
2624 success: false,
2625 message: Some("No matching outputs".into()),
2626 };
2627 }
2628 if monitor.is_none() {
2629 paused.store(false, Ordering::SeqCst);
2630 }
2631 for rs in targets {
2632 let rs_lock = rs.lock().await;
2633 rs_lock.video_playback.resume();
2634 rs_lock.gif_paused.store(false, Ordering::SeqCst);
2635 rs_lock.pacer.notify();
2636 }
2637 IpcResponse {
2638 success: true,
2639 message: Some("Resumed".into()),
2640 }
2641 }
2642 IpcCommand::Reload => {
2643 let fresh = crate::config::load_config(None)
2647 .map_err(|e| e.to_string())
2648 .and_then(|cfg| {
2649 validate_live_config(&cfg)?;
2650 Ok(cfg)
2651 });
2652 let fresh = match fresh {
2653 Ok(cfg) => cfg,
2654 Err(reason) => {
2655 return IpcResponse {
2656 success: false,
2657 message: Some(format!("Reload rejected: {reason}")),
2658 };
2659 }
2660 };
2661 let preload = clamp_preload_frames(fresh.video.preload_frames);
2662 let max_fps = clamp_max_fps(fresh.daemon.max_fps);
2663 let hw_accel =
2664 crate::video::HwAccel::from_config(&fresh.video.hw_decode);
2665 {
2666 let mut eng = engine.lock().await;
2667 eng.config = fresh.clone();
2668 if let Err(e) = eng.reload() {
2669 return IpcResponse {
2670 success: false,
2671 message: Some(e.to_string()),
2672 };
2673 }
2674 }
2675 let targets = snapshot_all_named(&render_states_map).await;
2678 for (_, rs) in targets {
2679 let mut rs_lock = rs.lock().await;
2680 rs_lock.hw_accel = hw_accel;
2681 rs_lock.preload_frames = preload;
2682 rs_lock.max_fps = max_fps;
2683 }
2684 let _ = theme_gen;
2685 IpcResponse {
2686 success: true,
2687 message: Some("Reloaded".into()),
2688 }
2689 }
2690 IpcCommand::Preview {
2691 path,
2692 effect,
2693 duration_ms,
2694 no_theme,
2695 theme_override,
2696 monitor,
2697 scaling_mode,
2698 } => {
2699 if paused.load(Ordering::SeqCst) {
2700 return IpcResponse {
2701 success: false,
2702 message: Some("Daemon is paused".into()),
2703 };
2704 }
2705 let p = std::path::PathBuf::from(&path);
2706 if !is_usable_wallpaper_file(&p) {
2707 return IpcResponse {
2708 success: false,
2709 message: Some(format!("File not found or unreadable: {}", path)),
2710 };
2711 }
2712
2713 let targets =
2717 snapshot_named_targets(&render_states_map, monitor.as_deref()).await;
2718 if targets.is_empty() {
2719 return IpcResponse {
2720 success: false,
2721 message: match &monitor {
2722 Some(name) => Some(format!("Unknown monitor: {name}")),
2723 None => Some("No outputs available".into()),
2724 },
2725 };
2726 }
2727
2728 let effect = effect.unwrap_or_else(|| {
2729 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
2730 });
2731 let is_video = crate::video::VideoDecoder::is_video_file(&p);
2735 let duration = duration_ms
2736 .unwrap_or(if is_video { 150 } else { 2000 })
2737 .min(crate::ipc::MAX_DURATION_MS);
2738 let sm = scaling_mode.unwrap_or(crate::config::ScalingMode::Fill);
2739 let scaling_mode_u32 = match sm {
2740 crate::config::ScalingMode::Fill => 0u32,
2741 crate::config::ScalingMode::Fit => 1,
2742 crate::config::ScalingMode::Stretch => 2,
2743 crate::config::ScalingMode::Center => 3,
2744 crate::config::ScalingMode::Tile => 4,
2745 };
2746
2747 let mut last_err = None;
2748 for (name, rs) in &targets {
2749 let result = set_wallpaper_with_retry(
2750 rs,
2751 &p,
2752 &effect,
2753 duration,
2754 scaling_mode_u32,
2755 )
2756 .await;
2757
2758 match result {
2759 Err(e) => last_err = Some(format!("Render failed: {e}")),
2760 Ok(()) => {
2761 if let Err(err) = persist_wallpaper(name, &p) {
2762 tracing::warn!(
2763 "Wallpaper changed on {name}, but state persistence failed: {err}"
2764 );
2765 }
2766 }
2767 }
2768 }
2769
2770 match last_err {
2771 Some(e) => IpcResponse {
2772 success: false,
2773 message: Some(e),
2774 },
2775 None => {
2776 let generation =
2782 theme_gen.fetch_add(1, Ordering::SeqCst) + 1;
2783 let opts = SetOptions {
2784 no_theme,
2785 theme_provider: theme_override,
2786 monitor,
2787 };
2788 let eng = engine.clone();
2789 let theme_gen_check = theme_gen.clone();
2790 let p_clone = p.clone();
2791 tokio::spawn(async move {
2792 if theme_gen_check.load(Ordering::SeqCst) != generation {
2793 return;
2794 }
2795 let mut eng = eng.lock().await;
2796 if theme_gen_check.load(Ordering::SeqCst) != generation {
2797 return;
2798 }
2799 if let Err(e) =
2800 eng.set_wallpaper(&p_clone, &opts).await
2801 {
2802 tracing::warn!(
2803 "Post-render hooks/theme failed for {p_clone:?}: {e}"
2804 );
2805 }
2806 });
2807 IpcResponse {
2808 success: true,
2809 message: None,
2810 }
2811 }
2812 }
2813 }
2814 IpcCommand::Stop => {
2815 let targets = snapshot_all_named(&render_states_map).await;
2816 for (_, rs) in targets {
2817 let state = rs.lock().await;
2818 state.playback_gen.fetch_add(1, Ordering::SeqCst);
2819 state.pacer.notify();
2820 state.video_playback.stop();
2821 }
2822 let sp = stop_socket.clone();
2823 tokio::spawn(async move {
2824 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
2825 let _ = std::fs::remove_file(&sp);
2826 std::process::exit(0);
2827 });
2828 IpcResponse {
2829 success: true,
2830 message: Some("Stopping".into()),
2831 }
2832 }
2833 IpcCommand::Status => {
2834 let state = if paused.load(Ordering::SeqCst) {
2835 "paused"
2836 } else {
2837 "running"
2838 };
2839 IpcResponse {
2840 success: true,
2841 message: Some(format!("wallr daemon {}", state)),
2842 }
2843 }
2844 IpcCommand::Seek {
2845 timestamp_ms,
2846 monitor,
2847 } => {
2848 let targets =
2849 snapshot_named_targets(&render_states_map, monitor.as_deref()).await;
2850 if targets.is_empty() {
2851 return IpcResponse {
2852 success: false,
2853 message: match &monitor {
2854 Some(name) => Some(format!("Unknown monitor: {name}")),
2855 None => Some("No outputs available".into()),
2856 },
2857 };
2858 }
2859 let mut seek_count = 0u32;
2863 let mut errors = Vec::new();
2864 for (name, rs) in &targets {
2865 let rs_lock = rs.lock().await;
2866 match rs_lock
2867 .video_playback
2868 .seek(std::time::Duration::from_millis(timestamp_ms))
2869 {
2870 Ok(()) => {
2871 seek_count += 1;
2872 }
2873 Err(e) => {
2874 errors.push(format!("{}: {}", name, e));
2875 }
2876 }
2877 }
2878 if seek_count == 0 {
2879 IpcResponse {
2880 success: false,
2881 message: Some(format!(
2882 "Seek failed on all outputs: {}",
2883 errors.join("; ")
2884 )),
2885 }
2886 } else if !errors.is_empty() {
2887 IpcResponse {
2888 success: true,
2889 message: Some(format!(
2890 "Seeked {} output(s) to {}ms, {} failed: {}",
2891 seek_count,
2892 timestamp_ms,
2893 errors.len(),
2894 errors.join("; ")
2895 )),
2896 }
2897 } else {
2898 IpcResponse {
2899 success: true,
2900 message: Some(format!(
2901 "Seeked {} output(s) to {}ms",
2902 seek_count, timestamp_ms
2903 )),
2904 }
2905 }
2906 }
2907 IpcCommand::Info { monitor } => {
2908 let targets =
2909 snapshot_named_targets(&render_states_map, monitor.as_deref()).await;
2910 if targets.is_empty() {
2911 return IpcResponse {
2912 success: false,
2913 message: match &monitor {
2914 Some(name) => Some(format!("Unknown monitor: {name}")),
2915 None => Some("No outputs available".into()),
2916 },
2917 };
2918 }
2919
2920 let mut lines = vec![
2921 format!("wallr v{}", env!("CARGO_PKG_VERSION")),
2922 String::new(),
2923 format!("Outputs: {}", targets.len()),
2924 ];
2925 for (name, _) in &targets {
2926 lines.push(format!(" - {name}"));
2927 }
2928
2929 for (name, rs) in &targets {
2931 let rs_lock = rs.lock().await;
2932 let gpu_info = rs_lock.gpu.as_ref().map_or_else(
2936 || "GPU: not initialized (static content)".to_string(),
2937 |gpu| {
2938 crate::video::gpu::adapter_diagnostics(&gpu.renderer.adapter)
2939 },
2940 );
2941
2942 lines.push(String::new());
2943 lines.push(format!("[{name}] {}x{}", rs_lock.width, rs_lock.height));
2944 lines.push(gpu_info);
2945
2946 match rs_lock.video_playback.metadata() {
2947 Some(meta) => {
2948 let decoder_info = rs_lock.video_playback.decoder_info();
2949 let hw = rs_lock.video_playback.hw_accel_in_use();
2950 let state = if rs_lock.video_playback.is_paused() {
2951 "paused"
2952 } else {
2953 "playing"
2954 };
2955 let position = rs_lock
2956 .video_playback
2957 .position()
2958 .map(|p| format!("{:.2}s", p.as_secs_f64()))
2959 .unwrap_or_else(|| "?".to_string());
2960 lines.push(String::new());
2961 lines.push("Video:".into());
2962 lines.push(format!(
2963 " Resolution: {}x{}",
2964 meta.width, meta.height
2965 ));
2966 lines.push(format!(" FPS: {:.2}", meta.fps));
2967 lines.push(format!(
2968 " Duration: {:.2}s",
2969 meta.duration.as_secs_f64()
2970 ));
2971 lines.push(format!(
2972 " Codec: {}",
2973 decoder_info
2974 .as_ref()
2975 .map(|d| d.codec_name.as_str())
2976 .unwrap_or("unknown")
2977 ));
2978 lines.push(format!(" Container: {}", meta.format));
2979 lines.push(format!(" Decoder: {}", hw.name()));
2980 if let Some(info) = decoder_info.as_ref() {
2981 lines.push(format!(" Decoder State: {}", info.state.name()));
2982 }
2983 lines.push(format!(
2984 " GPU Decode: {}",
2985 if hw == crate::video::HwAccel::Software {
2986 "disabled"
2987 } else {
2988 "enabled"
2989 }
2990 ));
2991 if let Some(info) = decoder_info.as_ref()
2992 && info.dropped_frames > 0
2993 {
2994 lines.push(format!(
2995 " Dropped Frames: {}",
2996 info.dropped_frames
2997 ));
2998 }
2999 lines.push(format!(" State: {} @ {}", state, position));
3000 }
3001 None => {
3002 lines.push(String::new());
3003 lines.push("Video: none active".into());
3004 lines.push("Decoder: idle".into());
3005 }
3006 }
3007 }
3008
3009 IpcResponse {
3010 success: true,
3011 message: Some(lines.join("\n")),
3012 }
3013 }
3014 IpcCommand::MonitorList => {
3015 let targets = snapshot_all_named(&render_states_map).await;
3016 let mut lines = Vec::new();
3017 for (name, rs) in &targets {
3018 let lock = rs.lock().await;
3019 lines.push(format!("{}: {}x{}", name, lock.width, lock.height));
3020 }
3021 if lines.is_empty() {
3022 IpcResponse {
3023 success: true,
3024 message: Some("No monitors connected".into()),
3025 }
3026 } else {
3027 IpcResponse {
3028 success: true,
3029 message: Some(lines.join("\n")),
3030 }
3031 }
3032 }
3033 IpcCommand::MonitorCurrent => {
3034 let targets = snapshot_all_named(&render_states_map).await;
3036 if let Some((name, rs)) = targets.first() {
3037 let lock = rs.lock().await;
3038 IpcResponse {
3039 success: true,
3040 message: Some(format!("{}: {}x{}", name, lock.width, lock.height)),
3041 }
3042 } else {
3043 IpcResponse {
3044 success: false,
3045 message: Some("No monitors connected".into()),
3046 }
3047 }
3048 }
3049 IpcCommand::Blank {
3050 monitor,
3051 effect,
3052 duration_ms,
3053 } => {
3054 let targets =
3055 snapshot_named_targets(&render_states_map, monitor.as_deref()).await;
3056 if targets.is_empty() {
3057 return IpcResponse {
3058 success: false,
3059 message: match &monitor {
3060 Some(name) => Some(format!("Unknown monitor: {name}")),
3061 None => Some("No outputs available".into()),
3062 },
3063 };
3064 }
3065 let mut blanked_count = 0u32;
3066 let mut errors = Vec::new();
3067 let black_effect = effect.unwrap_or_else(|| {
3068 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
3069 });
3070 let duration = duration_ms.unwrap_or(800);
3071 let mut blank_file = match tempfile::Builder::new()
3072 .prefix("wallr_blank-")
3073 .suffix(".png")
3074 .tempfile()
3075 {
3076 Ok(file) => file,
3077 Err(e) => {
3078 return IpcResponse {
3079 success: false,
3080 message: Some(format!("Failed to create blank image: {e}")),
3081 };
3082 }
3083 };
3084 let blank_path = blank_file.path().to_path_buf();
3085 let blank = image::DynamicImage::ImageRgba8(
3086 image::RgbaImage::from_pixel(1, 1, image::Rgba([0, 0, 0, 255])),
3087 );
3088 if let Err(e) = blank
3089 .write_to(blank_file.as_file_mut(), image::ImageFormat::Png)
3090 {
3091 return IpcResponse {
3092 success: false,
3093 message: Some(format!("Failed to write blank image: {e}")),
3094 };
3095 }
3096
3097 for (name, rs) in &targets {
3098 let rs = Arc::clone(rs);
3099 let blank_path = blank_path.clone();
3100 let black_effect = black_effect.clone();
3101 let blanked = tokio::task::spawn_blocking(move || {
3102 let mut lock = rs.blocking_lock();
3103 if lock.blanked {
3104 return Ok(false);
3105 }
3106 let previous = (
3107 lock.last_wallpaper.clone().unwrap_or_default(),
3108 lock.scaling_mode,
3109 );
3110 lock.set_wallpaper(&blank_path, &black_effect, duration, 0)?;
3111 lock.pre_blank = Some(previous);
3112 lock.blanked = true;
3113 Ok::<bool, anyhow::Error>(true)
3114 })
3115 .await;
3116 match blanked {
3117 Ok(Ok(true)) => blanked_count += 1,
3118 Ok(Ok(false)) => {}
3119 Ok(Err(e)) => errors.push(format!("{name}: blank failed: {e}")),
3120 Err(e) => errors.push(format!("{name}: blank task failed: {e}")),
3121 }
3122 }
3123 if errors.is_empty() {
3124 IpcResponse {
3125 success: true,
3126 message: Some(format!("Blanked {blanked_count} output(s)")),
3127 }
3128 } else {
3129 IpcResponse {
3130 success: false,
3131 message: Some(format!(
3132 "Blanked {blanked_count} output(s), {} error(s): {}",
3133 errors.len(),
3134 errors.join("; ")
3135 )),
3136 }
3137 }
3138 }
3139 IpcCommand::Restore {
3140 monitor,
3141 effect,
3142 duration_ms,
3143 } => {
3144 let targets =
3145 snapshot_named_targets(&render_states_map, monitor.as_deref()).await;
3146 if targets.is_empty() {
3147 return IpcResponse {
3148 success: false,
3149 message: match &monitor {
3150 Some(name) => Some(format!("Unknown monitor: {name}")),
3151 None => Some("No outputs available".into()),
3152 },
3153 };
3154 }
3155 let mut restored_count = 0u32;
3156 let mut errors = Vec::new();
3157 let restore_effect = effect.unwrap_or_else(|| {
3158 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
3159 });
3160 let duration = duration_ms.unwrap_or(800).min(crate::ipc::MAX_DURATION_MS);
3161
3162 for (name, rs) in &targets {
3163 let rs = Arc::clone(rs);
3164 let restore_effect = restore_effect.clone();
3165 let result = tokio::task::spawn_blocking(move || {
3166 let mut lock = rs.blocking_lock();
3167 if !lock.blanked {
3168 return None;
3169 }
3170 let result = match lock.pre_blank.clone() {
3171 Some((path, scaling_mode)) if path.exists() => lock
3172 .set_wallpaper(
3173 &path,
3174 &restore_effect,
3175 duration,
3176 scaling_mode,
3177 )
3178 .map_err(|e| format!("restore failed: {e}")),
3179 Some(_) => Err("wallpaper path no longer exists".to_string()),
3180 None => Err("no previous wallpaper to restore".to_string()),
3181 };
3182 if result.is_ok() {
3183 lock.blanked = false;
3184 lock.pre_blank = None;
3185 }
3186 Some(result)
3187 })
3188 .await;
3189 match result {
3190 Ok(Some(Ok(()))) => restored_count += 1,
3191 Ok(Some(Err(e))) => errors.push(format!("{name}: {e}")),
3192 Ok(None) => {}
3193 Err(e) => errors.push(format!("{name}: restore task failed: {e}")),
3194 }
3195 }
3196 if !errors.is_empty() {
3197 IpcResponse {
3198 success: restored_count > 0,
3199 message: Some(format!(
3200 "Restored {} output(s), {} error(s): {}",
3201 restored_count,
3202 errors.len(),
3203 errors.join("; ")
3204 )),
3205 }
3206 } else {
3207 IpcResponse {
3208 success: true,
3209 message: Some(format!("Restored {restored_count} output(s)")),
3210 }
3211 }
3212 }
3213 }
3214 }
3215 })
3216 .await?;
3217
3218 tokio::task::spawn_blocking(move || {
3219 loop {
3220 if let Err(e) = event_queue.blocking_dispatch(&mut wayland_state) {
3221 eprintln!("Wayland dispatch error: {e:?}");
3222 break;
3223 }
3224 }
3225 eprintln!("wallr: Wayland connection lost, exiting");
3229 std::process::exit(1);
3230 });
3231
3232 loop {
3233 tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
3234 }
3235 }
3236
3237 #[allow(clippy::too_many_arguments)]
3240 async fn create_render_state_for_output(
3241 renderer: &std::sync::Arc<std::sync::Mutex<Option<std::sync::Arc<Renderer>>>>,
3242 display_ptr: *mut std::ffi::c_void,
3243 wayland_state: &mut WaylandState,
3244 qh: &QueueHandle<WaylandState>,
3245 output: &OutputInfo,
3246 config: &WallrConfig,
3247 ) -> Result<RenderState, DaemonError> {
3248 let wl_surface = wayland_state.compositor_state.create_surface(qh);
3249 let layer_surface = wayland_state.layer_shell.create_layer_surface(
3250 qh,
3251 wl_surface,
3252 Layer::Background,
3253 Some("wallr"),
3254 Some(&output.wl_output),
3255 );
3256 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
3257 layer_surface.set_exclusive_zone(-1);
3258 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
3259
3260 let empty_region = wayland_state.compositor.create_region(qh, ());
3262 layer_surface
3263 .wl_surface()
3264 .set_input_region(Some(&empty_region));
3265 let output_id = output.wl_output.id().protocol_id();
3266 let viewport = wayland_state
3267 .viewporter
3268 .as_ref()
3269 .map(|viewporter| viewporter.get_viewport(layer_surface.wl_surface(), qh, ()));
3270 let scale_factor = if viewport.is_some() {
3271 1
3272 } else if output.scale_factor > 0 {
3273 output.scale_factor
3274 } else {
3275 1
3276 };
3277 layer_surface.wl_surface().set_buffer_scale(scale_factor);
3278 layer_surface.commit();
3279 empty_region.destroy();
3280 if let Some(viewport) = viewport {
3281 wayland_state.viewports.insert(output_id, viewport);
3282 }
3283
3284 let width = output.width;
3286 let height = output.height;
3287
3288 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
3289 let shm_surface = layer_surface.wl_surface().clone();
3290 wayland_state.surfaces.push((output_id, layer_surface));
3291
3292 Ok(RenderState {
3293 gpu: None,
3294 renderer: renderer.clone(),
3295 display_ptr: SendDisplayPtr(display_ptr),
3296 raw_surface: SendDisplayPtr(raw_surface),
3297 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
3298 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
3299 pacer: std::sync::Arc::new(LivePacer::new()),
3300 current_bind: None,
3301 current_tex: None,
3302 width,
3303 height,
3304 current_width: 0,
3305 current_height: 0,
3306 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
3307 hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
3308 preferred_gpu: config.video.preferred_gpu.clone(),
3309 preload_frames: clamp_preload_frames(config.video.preload_frames),
3310 max_fps: clamp_max_fps(config.daemon.max_fps),
3311 scaling_mode: 0,
3312 current_effect: None,
3313 last_wallpaper: None,
3314 pre_blank: None,
3315 blanked: false,
3316 gif_paused: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3317 shm_surface,
3318 shm_server: wayland_state.shm.wl_shm().clone(),
3319 shm_pool: SlotPool::new(INITIAL_SHM_POOL_BYTES, &wayland_state.shm).map_err(|e| {
3320 DaemonError::StartError(format!("shared-memory pool creation failed: {e}"))
3321 })?,
3322 shm_buffers: [None, None],
3323 shm_index: 0,
3324 shm_width: 0,
3325 shm_height: 0,
3326 gpu_surface_used: false,
3327 cached_video_texture: None,
3328 loop_video: config.wallpaper.loop_video,
3329 })
3330 }
3331}
3332
3333#[allow(clippy::too_many_arguments)]
3337fn create_render_state_for_output_sync(
3338 renderer: &std::sync::Arc<std::sync::Mutex<Option<std::sync::Arc<Renderer>>>>,
3339 display_ptr: *mut std::ffi::c_void,
3340 wayland_state: &mut WaylandState,
3341 qh: &QueueHandle<WaylandState>,
3342 output: &OutputInfo,
3343 config: &WallrConfig,
3344) -> Result<RenderState, DaemonError> {
3345 let wl_surface = wayland_state.compositor_state.create_surface(qh);
3346 let layer_surface = wayland_state.layer_shell.create_layer_surface(
3347 qh,
3348 wl_surface,
3349 Layer::Background,
3350 Some("wallr"),
3351 Some(&output.wl_output),
3352 );
3353 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
3354 layer_surface.set_exclusive_zone(-1);
3355 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
3356
3357 let empty_region = wayland_state.compositor.create_region(qh, ());
3358 layer_surface
3359 .wl_surface()
3360 .set_input_region(Some(&empty_region));
3361 let output_id = output.wl_output.id().protocol_id();
3362 let viewport = wayland_state
3363 .viewporter
3364 .as_ref()
3365 .map(|viewporter| viewporter.get_viewport(layer_surface.wl_surface(), qh, ()));
3366 let scale_factor = if viewport.is_some() {
3367 1
3368 } else if output.scale_factor > 0 {
3369 output.scale_factor
3370 } else {
3371 1
3372 };
3373 layer_surface.wl_surface().set_buffer_scale(scale_factor);
3374 layer_surface.commit();
3375 empty_region.destroy();
3376 if let Some(viewport) = viewport {
3377 wayland_state.viewports.insert(output_id, viewport);
3378 }
3379
3380 let width = output.width;
3382 let height = output.height;
3383
3384 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
3385 let shm_surface = layer_surface.wl_surface().clone();
3386 wayland_state.surfaces.push((output_id, layer_surface));
3387
3388 Ok(RenderState {
3389 gpu: None,
3390 renderer: renderer.clone(),
3391 display_ptr: SendDisplayPtr(display_ptr),
3392 raw_surface: SendDisplayPtr(raw_surface),
3393 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
3394 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
3395 pacer: std::sync::Arc::new(LivePacer::new()),
3396 current_bind: None,
3397 current_tex: None,
3398 width,
3399 height,
3400 current_width: 0,
3401 current_height: 0,
3402 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
3403 hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
3404 preferred_gpu: config.video.preferred_gpu.clone(),
3405 preload_frames: clamp_preload_frames(config.video.preload_frames),
3406 max_fps: clamp_max_fps(config.daemon.max_fps),
3407 scaling_mode: 0,
3408 current_effect: None,
3409 last_wallpaper: None,
3410 pre_blank: None,
3411 blanked: false,
3412 gif_paused: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
3413 shm_surface,
3414 shm_server: wayland_state.shm.wl_shm().clone(),
3415 shm_pool: SlotPool::new(INITIAL_SHM_POOL_BYTES, &wayland_state.shm).map_err(|e| {
3416 DaemonError::StartError(format!("shared-memory pool creation failed: {e}"))
3417 })?,
3418 shm_buffers: [None, None],
3419 shm_index: 0,
3420 shm_width: 0,
3421 shm_height: 0,
3422 gpu_surface_used: false,
3423 cached_video_texture: None,
3424 loop_video: config.wallpaper.loop_video,
3425 })
3426}