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