1use crate::config::WallrConfig;
2use crate::ipc::{IpcCommand, IpcResponse, start_ipc_server};
3use crate::renderer::Renderer;
4use crate::wallpaper::{SetOptions, WallpaperEngine};
5use notify::{Event, EventKind, RecursiveMode, Watcher};
6use std::path::PathBuf;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, Ordering};
9use tokio::sync::Mutex;
10
11use raw_window_handle::{
12 DisplayHandle, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle,
13 WaylandDisplayHandle, WaylandWindowHandle, WindowHandle,
14};
15use smithay_client_toolkit::{
16 compositor::{CompositorHandler, CompositorState},
17 delegate_compositor, delegate_layer, delegate_output, delegate_registry, delegate_shm,
18 output::{OutputHandler, OutputState},
19 registry::{ProvidesRegistryState, RegistryState},
20 registry_handlers,
21 shell::WaylandSurface,
22 shell::wlr_layer::{
23 Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
24 LayerSurfaceConfigure,
25 },
26 shm::{Shm, ShmHandler},
27};
28use wayland_client::{
29 Connection, Proxy, QueueHandle,
30 globals::registry_queue_init,
31 protocol::{wl_compositor, wl_output, wl_surface},
32};
33#[derive(Debug, thiserror::Error)]
34pub enum DaemonError {
35 #[error("daemon already running: {0}")]
36 AlreadyRunning(String),
37 #[error("failed to start daemon: {0}")]
38 StartError(String),
39 #[error("I/O error: {0}")]
40 Io(#[from] std::io::Error),
41 #[error("IPC error: {0}")]
42 Ipc(#[from] crate::ipc::IpcError),
43 #[error("Config error: {0}")]
44 Config(#[from] crate::config::ConfigError),
45 #[error("Wallpaper error: {0}")]
46 Wallpaper(#[from] crate::wallpaper::WallpaperError),
47}
48
49pub struct WaylandWindow {
50 pub display: *mut std::ffi::c_void,
51 pub surface: *mut std::ffi::c_void,
52}
53
54unsafe impl Send for WaylandWindow {}
55unsafe impl Sync for WaylandWindow {}
56
57impl HasWindowHandle for WaylandWindow {
58 fn window_handle(&self) -> Result<WindowHandle<'_>, raw_window_handle::HandleError> {
59 let surface = std::ptr::NonNull::new(self.surface)
60 .ok_or(raw_window_handle::HandleError::Unavailable)?;
61 let handle = WaylandWindowHandle::new(surface);
62 unsafe { Ok(WindowHandle::borrow_raw(RawWindowHandle::Wayland(handle))) }
63 }
64}
65
66impl HasDisplayHandle for WaylandWindow {
67 fn display_handle(&self) -> Result<DisplayHandle<'_>, raw_window_handle::HandleError> {
68 let display = std::ptr::NonNull::new(self.display)
69 .ok_or(raw_window_handle::HandleError::Unavailable)?;
70 let handle = WaylandDisplayHandle::new(display);
71 unsafe { Ok(DisplayHandle::borrow_raw(RawDisplayHandle::Wayland(handle))) }
72 }
73}
74
75struct OutputInfo {
76 name: String,
77 width: u32,
78 height: u32,
79 scale_factor: i32,
80 wl_output: wl_output::WlOutput,
81}
82
83struct WaylandState {
84 registry_state: RegistryState,
85 output_state: OutputState,
86 compositor_state: CompositorState,
87 shm: Shm,
88 outputs: std::collections::HashMap<u32, OutputInfo>,
89 surfaces: Vec<(u32, LayerSurface)>,
90 output_destroy_tx: tokio::sync::mpsc::UnboundedSender<String>,
93}
94
95impl ProvidesRegistryState for WaylandState {
96 fn registry(&mut self) -> &mut RegistryState {
97 &mut self.registry_state
98 }
99
100 registry_handlers![OutputState,];
101}
102
103impl CompositorHandler for WaylandState {
104 fn scale_factor_changed(
105 &mut self,
106 _conn: &Connection,
107 _qh: &QueueHandle<Self>,
108 _surface: &wl_surface::WlSurface,
109 _new_factor: i32,
110 ) {
111 }
112 fn transform_changed(
113 &mut self,
114 _conn: &Connection,
115 _qh: &QueueHandle<Self>,
116 _surface: &wl_surface::WlSurface,
117 _new_transform: wl_output::Transform,
118 ) {
119 }
120 fn frame(
121 &mut self,
122 _conn: &Connection,
123 _qh: &QueueHandle<Self>,
124 _surface: &wl_surface::WlSurface,
125 _time: u32,
126 ) {
127 }
128 fn surface_enter(
129 &mut self,
130 _conn: &Connection,
131 _qh: &QueueHandle<Self>,
132 _surface: &wl_surface::WlSurface,
133 _output: &wl_output::WlOutput,
134 ) {
135 }
136 fn surface_leave(
137 &mut self,
138 _conn: &Connection,
139 _qh: &QueueHandle<Self>,
140 _surface: &wl_surface::WlSurface,
141 _output: &wl_output::WlOutput,
142 ) {
143 }
144}
145
146impl wayland_client::Dispatch<wayland_client::protocol::wl_region::WlRegion, ()> for WaylandState {
147 fn event(
148 _state: &mut WaylandState,
149 _region: &wayland_client::protocol::wl_region::WlRegion,
150 _event: wayland_client::protocol::wl_region::Event,
151 _data: &(),
152 _conn: &Connection,
153 _qh: &QueueHandle<WaylandState>,
154 ) {
155 }
156}
157
158impl LayerShellHandler for WaylandState {
159 fn configure(
160 &mut self,
161 _conn: &Connection,
162 _qh: &QueueHandle<Self>,
163 layer: &LayerSurface,
164 _configure: LayerSurfaceConfigure,
165 _serial: u32,
166 ) {
167 layer.commit();
168 }
169
170 fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {}
171}
172
173impl ShmHandler for WaylandState {
174 fn shm_state(&mut self) -> &mut Shm {
175 &mut self.shm
176 }
177}
178
179impl OutputHandler for WaylandState {
180 fn output_state(&mut self) -> &mut OutputState {
181 &mut self.output_state
182 }
183 fn new_output(
184 &mut self,
185 _conn: &Connection,
186 _qh: &QueueHandle<Self>,
187 output: wl_output::WlOutput,
188 ) {
189 let id = output.id().protocol_id();
190 tracing::info!("Output detected: protocol_id={id}");
191 let info = OutputInfo {
192 name: format!("output-{id}"),
193 width: 1920,
194 height: 1080,
195 scale_factor: 1,
196 wl_output: output,
197 };
198 self.outputs.insert(id, info);
199 }
200 fn update_output(
201 &mut self,
202 _conn: &Connection,
203 _qh: &QueueHandle<Self>,
204 output: wl_output::WlOutput,
205 ) {
206 let id = output.id().protocol_id();
207 if let Some(info) = self.outputs.get_mut(&id) {
208 if let Some(mode) = self
209 .output_state
210 .info(&output)
211 .and_then(|i| i.modes.iter().find(|m| m.current).cloned())
212 {
213 info.width = mode.dimensions.0 as u32;
214 info.height = mode.dimensions.1 as u32;
215 }
216 if let Some(info_data) = self.output_state.info(&output) {
217 info.scale_factor = info_data.scale_factor;
218 let resolved = info_data
222 .name
223 .as_deref()
224 .filter(|n| !n.is_empty())
225 .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
226 if let Some(real_name) = resolved {
227 if real_name != info.name {
228 tracing::info!(
229 "Output {id}: resolved name '{}' -> '{}'",
230 info.name,
231 real_name
232 );
233 info.name = real_name.to_string();
234 }
235 } else if info.name.starts_with("output-")
236 && (!info_data.make.is_empty() || !info_data.model.is_empty())
237 {
238 let fallback = format!("{} {}", info_data.make, info_data.model)
239 .trim()
240 .to_string();
241 if !fallback.is_empty() {
242 tracing::info!(
243 "Output {id}: fallback name '{}' -> '{}'",
244 info.name,
245 fallback
246 );
247 info.name = fallback;
248 }
249 }
250 }
251 }
252 }
253 fn output_destroyed(
254 &mut self,
255 _conn: &Connection,
256 _qh: &QueueHandle<Self>,
257 output: wl_output::WlOutput,
258 ) {
259 let id = output.id().protocol_id();
260 if let Some(info) = self.outputs.remove(&id) {
261 tracing::info!("Output disconnected: {} (protocol_id={})", info.name, id);
262 let _ = self.output_destroy_tx.send(info.name);
263 }
264 }
265}
266
267delegate_compositor!(WaylandState);
268delegate_layer!(WaylandState);
269delegate_output!(WaylandState);
270delegate_registry!(WaylandState);
271delegate_shm!(WaylandState);
272
273struct LivePacer {
275 lock: std::sync::Mutex<()>,
276 cond: std::sync::Condvar,
277}
278
279impl LivePacer {
280 fn new() -> Self {
281 Self {
282 lock: std::sync::Mutex::new(()),
283 cond: std::sync::Condvar::new(),
284 }
285 }
286
287 fn notify(&self) {
288 let _guard = self.lock.lock().unwrap();
289 self.cond.notify_all();
290 }
291
292 fn wait_until(&self, deadline: std::time::Instant) {
295 let guard = self.lock.lock().unwrap();
296 let now = std::time::Instant::now();
297 if deadline <= now {
298 return;
299 }
300 let _ = self
301 .cond
302 .wait_timeout_while(guard, deadline - now, |_| true);
303 }
304}
305
306struct RenderState {
307 renderer: std::sync::Arc<Renderer>,
308 surface: &'static wgpu::Surface<'static>,
309 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
313 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
316 pacer: std::sync::Arc<LivePacer>,
319 current_bind: Option<wgpu::BindGroup>,
320 current_tex: Option<wgpu::Texture>,
321 width: u32,
322 height: u32,
323 current_width: u32,
324 current_height: u32,
325 format: wgpu::TextureFormat,
326 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
328 hw_accel: crate::video::HwAccel,
330 scaling_mode: u32,
332 per_output_uniforms: std::sync::Arc<crate::renderer::PerOutputUniforms>,
334 last_wallpaper: Option<std::path::PathBuf>,
336 pre_blank: Option<(std::path::PathBuf, u32)>,
338 blanked: bool,
340}
341
342struct CommitData {
345 bg_bind: wgpu::BindGroup,
346 new_bind: wgpu::BindGroup,
347 img_width: u32,
348 img_height: u32,
349 old_img_width: u32,
350 old_img_height: u32,
351 format: wgpu::TextureFormat,
352 width: u32,
353 height: u32,
354 animated: Option<crate::animated::AnimatedImage>,
357 is_video: bool,
359 generation: u64,
362 scaling_mode: u32,
364}
365
366impl RenderState {
367 async fn set_wallpaper(
368 &mut self,
369 path: &std::path::Path,
370 effect: &crate::animation::Effect,
371 duration_ms: u32,
372 scaling_mode: u32,
373 ) -> anyhow::Result<()> {
374 self.scaling_mode = scaling_mode;
375 let commit = self.commit_wallpaper(path, scaling_mode)?;
376 self.spawn_transition(commit, effect, duration_ms);
377 Ok(())
378 }
379
380 fn commit_wallpaper(
384 &mut self,
385 path: &std::path::Path,
386 scaling_mode: u32,
387 ) -> anyhow::Result<CommitData> {
388 use image::ImageReader;
389
390 if crate::video::VideoDecoder::is_video_file(path) {
392 tracing::info!("Video file detected: {:?}", path);
393
394 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
400 self.pacer.notify();
401
402 let metadata = self.video_playback.start(path, self.hw_accel)?;
405
406 let first_frame = self
409 .video_playback
410 .wait_first_frame(std::time::Duration::from_millis(1000));
411
412 let (new_tex, new_bind, img_width, img_height) = if let Some(frame) = first_frame {
413 let (tex, bind) = self.renderer.create_texture(frame.width, frame.height);
414 self.renderer
415 .update_texture(&tex, &frame.data, frame.width, frame.height);
416 (tex, bind, frame.width, frame.height)
417 } else {
418 tracing::warn!("No first frame available, using black texture");
420 let (tex, bind) = self
421 .renderer
422 .create_texture(metadata.width, metadata.height);
423 let black = vec![0u8; (metadata.width * metadata.height * 4) as usize];
424 self.renderer
425 .update_texture(&tex, &black, metadata.width, metadata.height);
426 (tex, bind, metadata.width, metadata.height)
427 };
428
429 let old_bind = self.current_bind.take();
430 let (old_img_width, old_img_height) = if old_bind.is_some() {
431 (self.current_width.max(1), self.current_height.max(1))
432 } else {
433 (img_width, img_height)
434 };
435 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
436
437 drop(self.current_tex.take());
438 self.current_tex = Some(new_tex);
439 self.current_bind = Some(new_bind.clone());
440 self.current_width = img_width;
441 self.current_height = img_height;
442
443 return Ok(CommitData {
444 bg_bind,
445 new_bind,
446 img_width,
447 img_height,
448 old_img_width,
449 old_img_height,
450 format: self.format,
451 width: self.width,
452 height: self.height,
453 animated: None,
454 is_video: true,
455 generation,
456 scaling_mode,
457 });
458 }
459
460 self.video_playback.stop();
464
465 let mut animated = crate::animated::AnimatedImage::decode(path)?;
468 let (new_tex, new_bind, img_width, img_height) = if let Some(anim) = animated.as_mut() {
469 let (w, h) = (anim.width, anim.height);
470 let (tex, bind) = self.renderer.create_texture(w, h);
471 let first = anim.first_frame();
472 if !first.is_empty() {
473 self.renderer.update_texture(&tex, first, w, h);
474 }
475 (tex, bind, w, h)
476 } else {
477 let new_img = ImageReader::open(path)?.decode()?;
478 let (tex, bind) = self.renderer.load_texture(&new_img)?;
479 (tex, bind, new_img.width(), new_img.height())
480 };
481
482 let old_bind = self.current_bind.take();
483 let (old_img_width, old_img_height) = if old_bind.is_some() {
484 (self.current_width.max(1), self.current_height.max(1))
485 } else {
486 (img_width, img_height)
487 };
488 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
493
494 drop(self.current_tex.take());
495 self.current_tex = Some(new_tex);
496 self.current_bind = Some(new_bind.clone());
497 self.current_width = img_width;
498 self.current_height = img_height;
499
500 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
501 self.pacer.notify();
502
503 Ok(CommitData {
504 bg_bind,
505 new_bind,
506 img_width,
507 img_height,
508 old_img_width,
509 old_img_height,
510 format: self.format,
511 width: self.width,
512 height: self.height,
513 animated,
514 is_video: false,
515 generation,
516 scaling_mode,
517 })
518 }
519
520 fn spawn_transition(
525 &self,
526 commit: CommitData,
527 effect: &crate::animation::Effect,
528 duration_ms: u32,
529 ) {
530 let renderer = self.renderer.clone();
531 let surface: &'static wgpu::Surface<'static> = self.surface;
532 let render_lock = self.render_lock.clone();
533 let playback_gen = self.playback_gen.clone();
534 let pacer = self.pacer.clone();
535 let video_playback = self.video_playback.clone();
536 let per_output_uniforms = std::sync::Arc::clone(&self.per_output_uniforms);
537 let effect = effect.clone();
538 drop(tokio::task::spawn_blocking(move || {
539 render_transition(
540 renderer,
541 surface,
542 render_lock,
543 playback_gen,
544 pacer,
545 video_playback,
546 commit,
547 effect,
548 duration_ms,
549 &per_output_uniforms,
550 );
551 }));
552 }
553}
554
555#[allow(clippy::too_many_arguments)]
563fn render_transition(
564 renderer: std::sync::Arc<Renderer>,
565 surface: &'static wgpu::Surface<'static>,
566 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
567 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
568 pacer: std::sync::Arc<LivePacer>,
569 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
570 mut commit: CommitData,
571 effect: crate::animation::Effect,
572 duration_ms: u32,
573 per_output_uniforms: &crate::renderer::PerOutputUniforms,
574) {
575 let _guard = render_lock
576 .lock()
577 .unwrap_or_else(|poisoned| poisoned.into_inner());
578
579 let duration = std::time::Duration::from_millis(u64::from(duration_ms.max(1)));
580 let start = std::time::Instant::now();
581 loop {
582 let progress = start.elapsed().as_secs_f32() / duration.as_secs_f32();
583 let uniforms = crate::animation::compute_effect_uniforms(&effect, progress.clamp(0.0, 1.0));
584 let status = renderer.render_frame(
585 crate::renderer::FrameRequest {
586 surface,
587 format: commit.format,
588 bg_bind: &commit.bg_bind,
589 new_bind: &commit.new_bind,
590 effect: &uniforms,
591 width: commit.width,
592 height: commit.height,
593 img_width: commit.img_width,
594 img_height: commit.img_height,
595 old_img_width: commit.old_img_width,
596 old_img_height: commit.old_img_height,
597 scaling_mode: commit.scaling_mode,
598 },
599 per_output_uniforms,
600 );
601 let status = match status {
602 Ok(status) => status,
603 Err(err) => {
604 eprintln!("wallr: transition render failed: {err}");
605 break;
606 }
607 };
608 if progress >= 1.0 || status == crate::renderer::FrameStatus::TimedOut {
609 break;
610 }
611 }
612
613 let mut animated = commit.animated.take();
617 if let Some(animated) = animated.as_mut()
618 && playback_gen.load(Ordering::SeqCst) == commit.generation
619 {
620 play_live(
621 &renderer,
622 surface,
623 &commit,
624 animated,
625 &playback_gen,
626 &pacer,
627 per_output_uniforms,
628 );
629 } else if commit.is_video && playback_gen.load(Ordering::SeqCst) == commit.generation {
630 play_video(
631 &renderer,
632 surface,
633 &commit,
634 &video_playback,
635 &playback_gen,
636 per_output_uniforms,
637 );
638 }
639}
640
641fn play_live(
647 renderer: &Renderer,
648 surface: &'static wgpu::Surface<'static>,
649 commit: &CommitData,
650 animated: &mut crate::animated::AnimatedImage,
651 playback_gen: &std::sync::atomic::AtomicU64,
652 pacer: &LivePacer,
653 per_output_uniforms: &crate::renderer::PerOutputUniforms,
654) {
655 let (tex_a, bind_a) = renderer.create_texture(animated.width, animated.height);
656 let (tex_b, bind_b) = renderer.create_texture(animated.width, animated.height);
657 let (frame_w, frame_h) = (animated.width, animated.height);
658 let (bytes_per_row, rows) = (frame_w * 4, frame_h);
659 let frame_bytes = bytes_per_row as u64 * rows as u64;
660
661 let direct_upload = bytes_per_row % 256 == 0;
664 let staging: Vec<wgpu::Buffer> = if direct_upload {
665 (0..2)
666 .map(|_| {
667 renderer.device.create_buffer(&wgpu::BufferDescriptor {
668 label: Some("wallr-gif-staging"),
669 size: frame_bytes,
670 usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
671 mapped_at_creation: false,
672 })
673 })
674 .collect()
675 } else {
676 Vec::new()
677 };
678
679 let first = animated.first_frame();
680 if !first.is_empty() {
681 renderer.update_texture(&tex_a, first, frame_w, frame_h);
682 renderer.update_texture(&tex_b, first, frame_w, frame_h);
683 }
684 let binds = [bind_a, bind_b];
685 let textures = [tex_a, tex_b];
686
687 let upload = |renderer: &Renderer,
690 tgt: usize,
691 index: usize,
692 slot: usize,
693 animated: &mut crate::animated::AnimatedImage|
694 -> bool {
695 if direct_upload {
696 let buffer = &staging[slot];
697 let slice = buffer.slice(..);
698 slice.map_async(wgpu::MapMode::Write, |_| {});
699 renderer.device.poll(wgpu::Maintain::Wait);
700 let ok = {
701 let mut mapped = slice.get_mapped_range_mut();
702 animated.decompress_into(index, &mut mapped)
703 };
704 buffer.unmap();
705 if ok {
706 let mut encoder = renderer
707 .device
708 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
709 encoder.copy_buffer_to_texture(
710 wgpu::TexelCopyBufferInfo {
711 buffer,
712 layout: wgpu::TexelCopyBufferLayout {
713 offset: 0,
714 bytes_per_row: Some(bytes_per_row),
715 rows_per_image: Some(rows),
716 },
717 },
718 wgpu::TexelCopyTextureInfo {
719 texture: &textures[tgt],
720 mip_level: 0,
721 origin: wgpu::Origin3d::ZERO,
722 aspect: wgpu::TextureAspect::All,
723 },
724 wgpu::Extent3d {
725 width: frame_w,
726 height: frame_h,
727 depth_or_array_layers: 1,
728 },
729 );
730 renderer.queue.submit([encoder.finish()]);
731 return true;
732 }
733 } else {
734 let frame = animated.frame_at(index);
735 if !frame.is_empty() {
736 renderer.update_texture(&textures[tgt], frame, frame_w, frame_h);
737 return true;
738 }
739 }
740 false
741 };
742
743 let mut cur = 0usize; let mut cur_frame = 0usize; let mut next_frame = 0usize; let mut slot = 0usize; let start = std::time::Instant::now();
748 let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
749 loop {
750 if playback_gen.load(Ordering::SeqCst) != commit.generation {
751 return;
752 }
753 let index = animated.frame_index_at(start.elapsed());
754 if index != cur_frame {
755 if next_frame != index {
756 upload(renderer, cur ^ 1, index, slot, animated);
757 slot ^= 1;
758 next_frame = index;
759 }
760 cur ^= 1;
761 cur_frame = index;
762 }
763 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
764 let status = renderer.render_frame(
765 crate::renderer::FrameRequest {
766 surface,
767 format: commit.format,
768 bg_bind: &binds[cur],
769 new_bind: &binds[cur],
770 effect: &uniforms,
771 width: commit.width,
772 height: commit.height,
773 img_width: animated.width,
774 img_height: animated.height,
775 old_img_width: animated.width,
776 old_img_height: animated.height,
777 scaling_mode: commit.scaling_mode,
778 },
779 per_output_uniforms,
780 );
781 match status {
782 Ok(crate::renderer::FrameStatus::Presented) => {}
783 _ => return,
784 }
785
786 let elapsed = start.elapsed();
794 let total: std::time::Duration = animated.total_duration();
795 let loops = (elapsed.as_millis() / total.as_millis().max(1)) as u64;
796 let next_change = animated.frame_start(index + 1) + total * (loops as u32);
797 let wait = next_change.saturating_sub(elapsed);
798 if wait > std::time::Duration::ZERO {
799 let next = index + 1;
800 if next_frame != next {
801 upload(renderer, cur ^ 1, next, slot, animated);
802 slot ^= 1;
803 next_frame = next;
804 }
805 pacer.wait_until(std::time::Instant::now() + wait);
806 }
807 }
808}
809
810fn play_video(
812 renderer: &Renderer,
813 surface: &'static wgpu::Surface<'static>,
814 commit: &CommitData,
815 video_playback: &std::sync::Arc<crate::video::VideoPlayback>,
816 playback_gen: &std::sync::atomic::AtomicU64,
817 per_output_uniforms: &crate::renderer::PerOutputUniforms,
818) {
819 let (width, height) = match video_playback.metadata() {
821 Some(meta) => (meta.width, meta.height),
822 None => {
823 tracing::warn!("No video metadata available");
824 return;
825 }
826 };
827
828 let (texture, bind) = renderer.create_texture(width, height);
829 let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
830
831 let mut uploaded = false;
834
835 loop {
836 if playback_gen.load(Ordering::SeqCst) != commit.generation {
841 return;
842 }
843
844 if let Some(frame) = video_playback.next_frame() {
847 if frame.width != width || frame.height != height {
850 continue;
851 }
852 renderer.update_texture(&texture, &frame.data, frame.width, frame.height);
853 uploaded = true;
854 }
855
856 if !uploaded {
857 std::thread::sleep(std::time::Duration::from_millis(2));
860 continue;
861 }
862
863 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
864 let status = renderer.render_frame(
865 crate::renderer::FrameRequest {
866 surface,
867 format: commit.format,
868 bg_bind: &bind,
869 new_bind: &bind,
870 effect: &uniforms,
871 width: commit.width,
872 height: commit.height,
873 img_width: width,
874 img_height: height,
875 old_img_width: width,
876 old_img_height: height,
877 scaling_mode: commit.scaling_mode,
878 },
879 per_output_uniforms,
880 );
881
882 match status {
883 Ok(crate::renderer::FrameStatus::Presented) => {}
884 other => {
887 tracing::warn!("Video present failed ({:?}), stopping playback", other);
888 video_playback.stop();
889 return;
890 }
891 }
892 }
893}
894
895async fn resolve_targets(
900 render_states: &std::collections::HashMap<
901 String,
902 std::sync::Arc<tokio::sync::Mutex<RenderState>>,
903 >,
904 monitor: Option<&str>,
905) -> Vec<std::sync::Arc<tokio::sync::Mutex<RenderState>>> {
906 match monitor {
907 Some(name) => {
908 if let Some(rs) = render_states.get(name) {
909 vec![rs.clone()]
910 } else {
911 Vec::new()
912 }
913 }
914 None => render_states.values().cloned().collect(),
915 }
916}
917
918pub struct Daemon {
919 config: WallrConfig,
920 paused: Arc<AtomicBool>,
921 engine: Arc<Mutex<WallpaperEngine>>,
922}
923
924impl Daemon {
925 pub fn new(config: WallrConfig) -> Result<Self, DaemonError> {
926 let engine = WallpaperEngine::new(config.clone())?;
927 Ok(Self {
928 config,
929 paused: Arc::new(AtomicBool::new(false)),
930 engine: Arc::new(Mutex::new(engine)),
931 })
932 }
933
934 pub async fn start(self) -> Result<(), DaemonError> {
935 let socket_path = crate::config::expand_path(&self.config.daemon.socket);
936 if socket_path.exists() {
937 if tokio::net::UnixStream::connect(&socket_path).await.is_ok() {
938 return Err(DaemonError::AlreadyRunning(
939 socket_path.to_string_lossy().to_string(),
940 ));
941 }
942 let _ = std::fs::remove_file(&socket_path);
943 }
944
945 let renderer = Renderer::new()
946 .await
947 .map_err(|e| DaemonError::StartError(format!("GPU init failed: {e}")))?;
948
949 let conn = Connection::connect_to_env()
950 .map_err(|e| DaemonError::StartError(format!("Failed to connect to Wayland: {e:?}")))?;
951 let backend = conn.backend();
952 let display_ptr = backend.display_ptr() as *mut std::ffi::c_void;
953
954 let (globals, mut event_queue) = registry_queue_init(&conn)
955 .map_err(|e| DaemonError::StartError(format!("registry_queue_init failed: {e:?}")))?;
956 let qh = event_queue.handle();
957
958 let compositor_state = CompositorState::bind(&globals, &qh)
959 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
960 let layer_shell = LayerShell::bind(&globals, &qh)
961 .map_err(|e| DaemonError::StartError(format!("layer_shell bind failed: {e:?}")))?;
962 let shm = Shm::bind(&globals, &qh)
963 .map_err(|e| DaemonError::StartError(format!("shm bind failed: {e:?}")))?;
964
965 let (output_destroy_tx, mut output_destroy_rx) =
966 tokio::sync::mpsc::unbounded_channel::<String>();
967
968 let mut wayland_state = WaylandState {
969 registry_state: RegistryState::new(&globals),
970 output_state: OutputState::new(&globals, &qh),
971 compositor_state,
972 shm,
973 outputs: std::collections::HashMap::new(),
974 surfaces: Vec::new(),
975 output_destroy_tx,
976 };
977
978 let compositor = globals
980 .bind::<wl_compositor::WlCompositor, WaylandState, smithay_client_toolkit::globals::GlobalData>(
981 &qh,
982 1..=4,
983 smithay_client_toolkit::globals::GlobalData,
984 )
985 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
986
987 for i in 0..5 {
991 event_queue
992 .roundtrip(&mut wayland_state)
993 .map_err(|e| DaemonError::StartError(format!("roundtrip {i} failed: {e:?}")))?;
994 }
995
996 if wayland_state.outputs.is_empty() {
997 return Err(DaemonError::StartError(
998 "no outputs detected after roundtrip".into(),
999 ));
1000 }
1001
1002 tracing::info!(
1003 "Detected {} output(s): {:?}",
1004 wayland_state.outputs.len(),
1005 wayland_state
1006 .outputs
1007 .values()
1008 .map(|o| format!("{} ({}x{})", o.name, o.width, o.height))
1009 .collect::<Vec<_>>()
1010 );
1011
1012 let renderer = std::sync::Arc::new(renderer);
1013
1014 let mut render_states: std::collections::HashMap<String, Arc<Mutex<RenderState>>> =
1018 std::collections::HashMap::new();
1019
1020 let output_info: Vec<(u32, OutputInfo)> = wayland_state
1023 .outputs
1024 .iter()
1025 .map(|(k, v)| {
1026 (
1027 *k,
1028 OutputInfo {
1029 name: v.name.clone(),
1030 width: v.width,
1031 height: v.height,
1032 scale_factor: v.scale_factor,
1033 wl_output: v.wl_output.clone(),
1034 },
1035 )
1036 })
1037 .collect();
1038
1039 for (proto_id, info) in &output_info {
1040 let name = info.name.clone();
1041 let rs = Self::create_render_state_for_output(
1042 &renderer,
1043 display_ptr,
1044 &mut wayland_state,
1045 &qh,
1046 &layer_shell,
1047 &compositor,
1048 info,
1049 &self.config,
1050 )
1051 .await?;
1052 let rs = Arc::new(Mutex::new(rs));
1053 render_states.insert(name.clone(), rs.clone());
1054
1055 let state_path = dirs::cache_dir()
1057 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1058 .join(format!("wallr/last_wallpaper/{name}"));
1059 if let Ok(path_str) = std::fs::read_to_string(&state_path) {
1060 let p = std::path::Path::new(path_str.trim());
1061 if p.exists() {
1062 let mut lock = rs.lock().await;
1063 let effect =
1064 crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1065 let _ = lock.set_wallpaper(p, &effect, 0, 0).await;
1066 }
1067 }
1068
1069 tracing::info!("Output ready: {name} ({proto_id})");
1070 }
1071
1072 let paused_clone = self.paused.clone();
1073 let engine_clone = self.engine.clone();
1074 let render_states_clone = render_states.clone();
1075
1076 {
1080 let rs_map = render_states.clone();
1081 let socket_path = socket_path.clone();
1082 tokio::spawn(async move {
1083 use tokio::signal::unix::{SignalKind, signal};
1084 let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler");
1085 let mut int = signal(SignalKind::interrupt()).expect("SIGINT handler");
1086 let mut hup = signal(SignalKind::hangup()).expect("SIGHUP handler");
1087 tokio::select! {
1088 _ = term.recv() => {}
1089 _ = int.recv() => {}
1090 _ = hup.recv() => {}
1091 }
1092 tracing::info!("Signal received, shutting down gracefully");
1093 for rs in rs_map.values() {
1094 if let Ok(state) = rs.try_lock() {
1095 state.video_playback.stop();
1096 }
1097 }
1098 let _ = std::fs::remove_file(&socket_path);
1099 std::process::exit(0);
1100 });
1101 }
1102
1103 let ipc_socket_path = socket_path.clone();
1104 start_ipc_server(&socket_path, move |cmd| {
1105 let paused = paused_clone.clone();
1106 let engine = engine_clone.clone();
1107 let render_states = render_states_clone.clone();
1108 let stop_socket = ipc_socket_path.clone();
1109 async move {
1110 match cmd {
1111 IpcCommand::Pause { monitor } => {
1112 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1113 if targets.is_empty() {
1114 return IpcResponse {
1115 success: false,
1116 message: Some("No matching outputs".into()),
1117 };
1118 }
1119 if monitor.is_none() {
1120 paused.store(true, Ordering::SeqCst);
1121 }
1122 for rs in targets {
1123 let rs_lock = rs.lock().await;
1124 rs_lock.video_playback.pause();
1125 }
1126 IpcResponse {
1127 success: true,
1128 message: Some("Paused".into()),
1129 }
1130 }
1131 IpcCommand::Resume { monitor } => {
1132 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1133 if targets.is_empty() {
1134 return IpcResponse {
1135 success: false,
1136 message: Some("No matching outputs".into()),
1137 };
1138 }
1139 if monitor.is_none() {
1140 paused.store(false, Ordering::SeqCst);
1141 }
1142 for rs in targets {
1143 let rs_lock = rs.lock().await;
1144 rs_lock.video_playback.resume();
1145 }
1146 IpcResponse {
1147 success: true,
1148 message: Some("Resumed".into()),
1149 }
1150 }
1151 IpcCommand::Reload => {
1152 let lock = engine.lock().await;
1153 match lock.reload() {
1154 Ok(_) => IpcResponse {
1155 success: true,
1156 message: Some("Reloaded".into()),
1157 },
1158 Err(e) => IpcResponse {
1159 success: false,
1160 message: Some(e.to_string()),
1161 },
1162 }
1163 }
1164 IpcCommand::Preview {
1165 path,
1166 effect,
1167 duration_ms,
1168 no_theme,
1169 theme_override,
1170 monitor,
1171 scaling_mode,
1172 } => {
1173 if paused.load(Ordering::SeqCst) {
1174 return IpcResponse {
1175 success: false,
1176 message: Some("Daemon is paused".into()),
1177 };
1178 }
1179 let p = std::path::PathBuf::from(&path);
1180 if !p.exists() {
1181 return IpcResponse {
1182 success: false,
1183 message: Some(format!("File not found: {}", path)),
1184 };
1185 }
1186
1187 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1189 if targets.is_empty() {
1190 return IpcResponse {
1191 success: false,
1192 message: match &monitor {
1193 Some(name) => Some(format!("Unknown monitor: {name}")),
1194 None => Some("No outputs available".into()),
1195 },
1196 };
1197 }
1198
1199 {
1201 let persist_names: Vec<&str> = match &monitor {
1202 Some(name) => vec![name.as_str()],
1203 None => render_states.keys().map(|s| s.as_str()).collect(),
1204 };
1205 for name in persist_names {
1206 let state_path = dirs::cache_dir()
1207 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1208 .join(format!("wallr/last_wallpaper/{name}"));
1209 if let Some(parent) = state_path.parent() {
1210 let _ = std::fs::create_dir_all(parent);
1211 }
1212 let _ = std::fs::write(&state_path, &path);
1213 }
1214 }
1215
1216 let effect = effect.unwrap_or_else(|| {
1217 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
1218 });
1219 let is_video = crate::video::VideoDecoder::is_video_file(&p);
1223 let duration = duration_ms.unwrap_or(if is_video { 150 } else { 2000 });
1224 let sm = scaling_mode.unwrap_or(crate::config::ScalingMode::Fill);
1225 let scaling_mode_u32 = match sm {
1226 crate::config::ScalingMode::Fill => 0u32,
1227 crate::config::ScalingMode::Fit => 1,
1228 crate::config::ScalingMode::Stretch => 2,
1229 crate::config::ScalingMode::Center => 3,
1230 crate::config::ScalingMode::Tile => 4,
1231 };
1232
1233 let mut last_err = None;
1234 for rs in &targets {
1235 let rs_clone = rs.clone();
1236 let p_clone = p.clone();
1237 let effect_clone = effect.clone();
1238 let result = tokio::task::spawn_blocking(move || {
1239 let rt = tokio::runtime::Handle::current();
1240 rt.block_on(async {
1241 let mut lock = rs_clone.lock().await;
1242 lock.set_wallpaper(
1243 &p_clone,
1244 &effect_clone,
1245 duration,
1246 scaling_mode_u32,
1247 )
1248 .await
1249 })
1250 })
1251 .await;
1252
1253 if let Err(e) = result {
1254 last_err = Some(format!("Task spawn failed: {e}"));
1255 continue;
1256 }
1257 if let Err(e) = result.unwrap() {
1258 last_err = Some(format!("Render failed: {e}"));
1259 }
1260 }
1261
1262 match last_err {
1263 Some(e) => IpcResponse {
1264 success: false,
1265 message: Some(e),
1266 },
1267 None => {
1268 let opts = SetOptions {
1269 no_theme,
1270 theme_provider: theme_override,
1271 monitor,
1272 };
1273 let mut eng = engine.lock().await;
1274 match eng.set_wallpaper(&p, &opts).await {
1275 Ok(()) => IpcResponse {
1276 success: true,
1277 message: None,
1278 },
1279 Err(e) => IpcResponse {
1280 success: true,
1281 message: Some(format!(
1282 "Wallpaper set, but hooks/theme failed: {e}"
1283 )),
1284 },
1285 }
1286 }
1287 }
1288 }
1289 IpcCommand::Stop => {
1290 let sp = stop_socket.clone();
1291 tokio::spawn(async move {
1292 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1293 let _ = std::fs::remove_file(&sp);
1294 std::process::exit(0);
1295 });
1296 IpcResponse {
1297 success: true,
1298 message: Some("Stopping".into()),
1299 }
1300 }
1301 IpcCommand::Status => {
1302 let state = if paused.load(Ordering::SeqCst) {
1303 "paused"
1304 } else {
1305 "running"
1306 };
1307 IpcResponse {
1308 success: true,
1309 message: Some(format!("wallr daemon {}", state)),
1310 }
1311 }
1312 IpcCommand::Seek {
1313 timestamp_ms,
1314 monitor,
1315 } => {
1316 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1317 if targets.is_empty() {
1318 return IpcResponse {
1319 success: false,
1320 message: match &monitor {
1321 Some(name) => Some(format!("Unknown monitor: {name}")),
1322 None => Some("No outputs available".into()),
1323 },
1324 };
1325 }
1326 let target_rs = targets.into_iter().next().unwrap();
1328 let rs_lock = target_rs.lock().await;
1329 match rs_lock
1330 .video_playback
1331 .seek(std::time::Duration::from_millis(timestamp_ms))
1332 {
1333 Ok(()) => IpcResponse {
1334 success: true,
1335 message: Some(format!("Seeked to {}ms", timestamp_ms)),
1336 },
1337 Err(e) => IpcResponse {
1338 success: false,
1339 message: Some(format!("Seek failed: {}", e)),
1340 },
1341 }
1342 }
1343 IpcCommand::Info { monitor } => {
1344 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1345 if targets.is_empty() {
1346 return IpcResponse {
1347 success: false,
1348 message: match &monitor {
1349 Some(name) => Some(format!("Unknown monitor: {name}")),
1350 None => Some("No outputs available".into()),
1351 },
1352 };
1353 }
1354
1355 let mut lines = vec![
1356 format!("wallr v{}", env!("CARGO_PKG_VERSION")),
1357 String::new(),
1358 format!("Outputs: {}", render_states.len()),
1359 ];
1360 for name in render_states.keys() {
1361 lines.push(format!(" - {name}"));
1362 }
1363
1364 for (name, rs) in &render_states {
1366 if monitor.is_some() && monitor.as_deref() != Some(name.as_str()) {
1367 continue;
1368 }
1369 let rs_lock = rs.lock().await;
1370 let gpu_info =
1371 crate::video::gpu::adapter_diagnostics(&rs_lock.renderer.adapter);
1372
1373 lines.push(String::new());
1374 lines.push(format!("[{name}] {}x{}", rs_lock.width, rs_lock.height));
1375 lines.push(gpu_info);
1376
1377 match rs_lock.video_playback.metadata() {
1378 Some(meta) => {
1379 let decoder_info = rs_lock.video_playback.decoder_info();
1380 let hw = rs_lock.video_playback.hw_accel_in_use();
1381 let state = if rs_lock.video_playback.is_paused() {
1382 "paused"
1383 } else {
1384 "playing"
1385 };
1386 let position = rs_lock
1387 .video_playback
1388 .position()
1389 .map(|p| format!("{:.2}s", p.as_secs_f64()))
1390 .unwrap_or_else(|| "?".to_string());
1391 lines.push(String::new());
1392 lines.push("Video:".into());
1393 lines.push(format!(
1394 " Resolution: {}x{}",
1395 meta.width, meta.height
1396 ));
1397 lines.push(format!(" FPS: {:.2}", meta.fps));
1398 lines.push(format!(
1399 " Duration: {:.2}s",
1400 meta.duration.as_secs_f64()
1401 ));
1402 lines.push(format!(
1403 " Codec: {}",
1404 decoder_info
1405 .as_ref()
1406 .map(|d| d.codec_name.as_str())
1407 .unwrap_or("unknown")
1408 ));
1409 lines.push(format!(" Container: {}", meta.format));
1410 lines.push(format!(" Decoder: {}", hw.name()));
1411 lines.push(format!(
1412 " GPU Decode: {}",
1413 if hw == crate::video::HwAccel::Software {
1414 "disabled"
1415 } else {
1416 "enabled"
1417 }
1418 ));
1419 lines.push(format!(" State: {} @ {}", state, position));
1420 }
1421 None => {
1422 lines.push(String::new());
1423 lines.push("Video: none active".into());
1424 lines.push("Decoder: idle".into());
1425 }
1426 }
1427 }
1428
1429 IpcResponse {
1430 success: true,
1431 message: Some(lines.join("\n")),
1432 }
1433 }
1434 IpcCommand::MonitorList => {
1435 let mut lines = Vec::new();
1436 for (name, rs) in &render_states {
1437 let lock = rs.lock().await;
1438 lines.push(format!("{}: {}x{}", name, lock.width, lock.height));
1439 }
1440 if lines.is_empty() {
1441 IpcResponse {
1442 success: true,
1443 message: Some("No monitors connected".into()),
1444 }
1445 } else {
1446 IpcResponse {
1447 success: true,
1448 message: Some(lines.join("\n")),
1449 }
1450 }
1451 }
1452 IpcCommand::MonitorCurrent => {
1453 if let Some((name, rs)) = render_states.iter().next() {
1455 let lock = rs.lock().await;
1456 IpcResponse {
1457 success: true,
1458 message: Some(format!("{}: {}x{}", name, lock.width, lock.height)),
1459 }
1460 } else {
1461 IpcResponse {
1462 success: false,
1463 message: Some("No monitors connected".into()),
1464 }
1465 }
1466 }
1467 IpcCommand::Blank { monitor } => {
1468 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1469 if targets.is_empty() {
1470 return IpcResponse {
1471 success: false,
1472 message: match &monitor {
1473 Some(name) => Some(format!("Unknown monitor: {name}")),
1474 None => Some("No outputs available".into()),
1475 },
1476 };
1477 }
1478 let mut blanked_count = 0u32;
1479 for (name, rs) in &render_states {
1480 if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
1481 continue;
1482 }
1483 let mut lock = rs.lock().await;
1484 if lock.blanked {
1485 continue;
1486 }
1487 lock.pre_blank = Some((
1489 lock.last_wallpaper.clone().unwrap_or_default(),
1490 lock.scaling_mode,
1491 ));
1492 lock.blanked = true;
1493 let black_effect = crate::animation::Effect::Fade(
1495 crate::animation::FadeParams::default(),
1496 );
1497 let tmp = std::env::temp_dir().join("wallr_blank.png");
1499 {
1500 let img =
1501 image::RgbaImage::from_pixel(1, 1, image::Rgba([0, 0, 0, 255]));
1502 let _ = img.save(&tmp);
1503 }
1504 let _ = lock.set_wallpaper(&tmp, &black_effect, 200, 0).await;
1505 blanked_count += 1;
1506 }
1507 IpcResponse {
1508 success: true,
1509 message: Some(format!("Blanked {blanked_count} output(s)")),
1510 }
1511 }
1512 IpcCommand::Restore { monitor } => {
1513 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1514 if targets.is_empty() {
1515 return IpcResponse {
1516 success: false,
1517 message: match &monitor {
1518 Some(name) => Some(format!("Unknown monitor: {name}")),
1519 None => Some("No outputs available".into()),
1520 },
1521 };
1522 }
1523 let mut restored_count = 0u32;
1524 for (name, rs) in &render_states {
1525 if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
1526 continue;
1527 }
1528 let mut lock = rs.lock().await;
1529 if !lock.blanked {
1530 continue;
1531 }
1532 lock.blanked = false;
1533 if let Some((ref path, scaling_mode)) = lock.pre_blank.clone() {
1534 let restore_effect = crate::animation::Effect::Fade(
1535 crate::animation::FadeParams::default(),
1536 );
1537 let _ = lock
1538 .set_wallpaper(
1539 std::path::Path::new(&path),
1540 &restore_effect,
1541 200,
1542 scaling_mode,
1543 )
1544 .await;
1545 }
1546 lock.pre_blank = None;
1547 restored_count += 1;
1548 }
1549 IpcResponse {
1550 success: true,
1551 message: Some(format!("Restored {restored_count} output(s)")),
1552 }
1553 }
1554 }
1555 }
1556 })
1557 .await?;
1558
1559 let mut render_states_cleanup = render_states.clone();
1561 tokio::spawn(async move {
1562 while let Some(name) = output_destroy_rx.recv().await {
1563 tracing::info!("Cleaning up render state for removed output: {name}");
1564 if let Some(rs) = render_states_cleanup.remove(&name) {
1565 let lock = rs.lock().await;
1566 lock.video_playback.stop();
1567 }
1568 }
1569 });
1570
1571 if self.config.watch.enabled
1573 && let Some(ref watch_dir) = self.config.watch.dir
1574 {
1575 let watch_path = crate::config::expand_path(watch_dir);
1576 self.start_watcher(watch_path, render_states).await?;
1577 }
1578
1579 tokio::task::spawn_blocking(move || {
1580 loop {
1581 if let Err(e) = event_queue.blocking_dispatch(&mut wayland_state) {
1582 eprintln!("Wayland dispatch error: {e:?}");
1583 break;
1584 }
1585 }
1586 eprintln!("wallr: Wayland connection lost, exiting");
1590 std::process::exit(1);
1591 });
1592
1593 loop {
1594 tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
1595 }
1596 }
1597
1598 async fn start_watcher(
1599 &self,
1600 dir: PathBuf,
1601 render_states: std::collections::HashMap<String, Arc<Mutex<RenderState>>>,
1602 ) -> Result<(), DaemonError> {
1603 let engine = self.engine.clone();
1604 let paused = self.paused.clone();
1605 let debounce = crate::config::parse_duration(&self.config.watch.debounce)
1606 .unwrap_or(std::time::Duration::from_millis(500));
1607
1608 let (tx, mut rx) = tokio::sync::mpsc::channel(100);
1609
1610 let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
1611 if let Ok(event) = res
1612 && let EventKind::Create(_) = event.kind
1613 {
1614 for path in event.paths {
1615 let _ = tx.blocking_send(path);
1616 }
1617 }
1618 })
1619 .map_err(|e| DaemonError::StartError(e.to_string()))?;
1620
1621 watcher
1622 .watch(&dir, RecursiveMode::NonRecursive)
1623 .map_err(|e| DaemonError::StartError(e.to_string()))?;
1624
1625 tokio::spawn(async move {
1626 let _watcher = watcher;
1627 let mut last: Option<(PathBuf, std::time::Instant)> = None;
1628
1629 while let Some(path) = rx.recv().await {
1630 if paused.load(Ordering::SeqCst) {
1631 continue;
1632 }
1633 if let Some((ref lp, ref lt)) = last
1634 && lp == &path
1635 && lt.elapsed() < debounce
1636 {
1637 continue;
1638 }
1639 let ext = path
1640 .extension()
1641 .unwrap_or_default()
1642 .to_string_lossy()
1643 .to_lowercase();
1644 if !["jpg", "jpeg", "png", "gif", "webp"].contains(&ext.as_str()) {
1645 continue;
1646 }
1647 last = Some((path.clone(), std::time::Instant::now()));
1648
1649 for (name, rs) in &render_states {
1651 let rs = rs.clone();
1652 let eng = engine.clone();
1653 let p = path.clone();
1654 let name = name.clone();
1655 tokio::spawn(async move {
1656 let mut lock = rs.lock().await;
1657 let effect =
1658 crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1659 let _ = lock.set_wallpaper(&p, &effect, 600, 0).await;
1660 drop(lock);
1661 let opts = SetOptions {
1662 no_theme: false,
1663 theme_provider: None,
1664 monitor: Some(name),
1665 };
1666 let mut elock = eng.lock().await;
1667 let _ = elock.set_wallpaper(&p, &opts).await;
1668 });
1669 }
1670 }
1671 });
1672
1673 Ok(())
1674 }
1675
1676 #[allow(clippy::too_many_arguments)]
1679 async fn create_render_state_for_output(
1680 renderer: &std::sync::Arc<Renderer>,
1681 display_ptr: *mut std::ffi::c_void,
1682 wayland_state: &mut WaylandState,
1683 qh: &QueueHandle<WaylandState>,
1684 layer_shell: &LayerShell,
1685 compositor: &wl_compositor::WlCompositor,
1686 output: &OutputInfo,
1687 config: &WallrConfig,
1688 ) -> Result<RenderState, DaemonError> {
1689 let wl_surface = wayland_state.compositor_state.create_surface(qh);
1690 let layer_surface = layer_shell.create_layer_surface(
1691 qh,
1692 wl_surface,
1693 Layer::Background,
1694 Some("wallr"),
1695 Some(&output.wl_output),
1696 );
1697 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
1698 layer_surface.set_exclusive_zone(-1);
1699 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
1700
1701 let empty_region = compositor.create_region(qh, ());
1703 layer_surface
1704 .wl_surface()
1705 .set_input_region(Some(&empty_region));
1706 layer_surface.commit();
1707 empty_region.destroy();
1708
1709 let scale_factor = if output.scale_factor > 0 {
1710 output.scale_factor
1711 } else {
1712 1
1713 };
1714 layer_surface.wl_surface().set_buffer_scale(scale_factor);
1715
1716 let width = output.width * scale_factor as u32;
1717 let height = output.height * scale_factor as u32;
1718
1719 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
1720 wayland_state
1721 .surfaces
1722 .push((output.wl_output.id().protocol_id(), layer_surface));
1723
1724 let window_handle = WaylandWindow {
1725 display: display_ptr,
1726 surface: raw_surface,
1727 };
1728
1729 let wgpu_surface = renderer
1730 .instance
1731 .create_surface(&window_handle)
1732 .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
1733
1734 let adapter = renderer
1735 .instance
1736 .request_adapter(&wgpu::RequestAdapterOptions {
1737 compatible_surface: Some(&wgpu_surface),
1738 power_preference: wgpu::PowerPreference::HighPerformance,
1739 force_fallback_adapter: false,
1740 })
1741 .await;
1742 let surf_format = adapter
1743 .as_ref()
1744 .map(|a| {
1745 let caps = wgpu_surface.get_capabilities(a);
1746 caps.formats
1747 .into_iter()
1748 .next()
1749 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
1750 })
1751 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb);
1752
1753 let surf_config = wgpu::SurfaceConfiguration {
1754 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1755 format: surf_format,
1756 width,
1757 height,
1758 present_mode: wgpu::PresentMode::Fifo,
1759 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
1760 view_formats: vec![],
1761 desired_maximum_frame_latency: 2,
1762 };
1763 wgpu_surface.configure(&renderer.device, &surf_config);
1764
1765 let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
1768 let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
1769
1770 Ok(RenderState {
1771 renderer: renderer.clone(),
1772 surface,
1773 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
1774 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
1775 pacer: std::sync::Arc::new(LivePacer::new()),
1776 current_bind: None,
1777 current_tex: None,
1778 width,
1779 height,
1780 current_width: 0,
1781 current_height: 0,
1782 format: surf_format,
1783 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
1784 hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
1785 scaling_mode: 0,
1786 per_output_uniforms: std::sync::Arc::new(renderer.create_per_output_uniforms()),
1787 last_wallpaper: None,
1788 pre_blank: None,
1789 blanked: false,
1790 })
1791 }
1792}