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
75#[derive(Clone)]
76struct OutputInfo {
77 name: String,
78 width: u32,
79 height: u32,
80 scale_factor: i32,
81 wl_output: wl_output::WlOutput,
82}
83
84struct WaylandState {
85 registry_state: RegistryState,
86 output_state: OutputState,
87 compositor_state: CompositorState,
88 shm: Shm,
89 outputs: std::collections::HashMap<u32, OutputInfo>,
90 surfaces: Vec<(u32, LayerSurface)>,
91 layer_shell: LayerShell,
93 compositor: wl_compositor::WlCompositor,
95 hotplug: Option<DaemonHotplug>,
98}
99
100struct SendDisplayPtr(*mut std::ffi::c_void);
103unsafe impl Send for SendDisplayPtr {}
104
105struct DaemonHotplug {
109 renderer: std::sync::Arc<Renderer>,
110 config: crate::config::WallrConfig,
111 display_ptr: SendDisplayPtr,
112 render_states: std::sync::Arc<
117 tokio::sync::Mutex<
118 std::collections::HashMap<String, std::sync::Arc<tokio::sync::Mutex<RenderState>>>,
119 >,
120 >,
121}
122
123impl ProvidesRegistryState for WaylandState {
124 fn registry(&mut self) -> &mut RegistryState {
125 &mut self.registry_state
126 }
127
128 registry_handlers![OutputState,];
129}
130
131impl CompositorHandler for WaylandState {
132 fn scale_factor_changed(
133 &mut self,
134 _conn: &Connection,
135 _qh: &QueueHandle<Self>,
136 _surface: &wl_surface::WlSurface,
137 _new_factor: i32,
138 ) {
139 }
140 fn transform_changed(
141 &mut self,
142 _conn: &Connection,
143 _qh: &QueueHandle<Self>,
144 _surface: &wl_surface::WlSurface,
145 _new_transform: wl_output::Transform,
146 ) {
147 }
148 fn frame(
149 &mut self,
150 _conn: &Connection,
151 _qh: &QueueHandle<Self>,
152 _surface: &wl_surface::WlSurface,
153 _time: u32,
154 ) {
155 }
156 fn surface_enter(
157 &mut self,
158 _conn: &Connection,
159 _qh: &QueueHandle<Self>,
160 _surface: &wl_surface::WlSurface,
161 _output: &wl_output::WlOutput,
162 ) {
163 }
164 fn surface_leave(
165 &mut self,
166 _conn: &Connection,
167 _qh: &QueueHandle<Self>,
168 _surface: &wl_surface::WlSurface,
169 _output: &wl_output::WlOutput,
170 ) {
171 }
172}
173
174impl wayland_client::Dispatch<wayland_client::protocol::wl_region::WlRegion, ()> for WaylandState {
175 fn event(
176 _state: &mut WaylandState,
177 _region: &wayland_client::protocol::wl_region::WlRegion,
178 _event: wayland_client::protocol::wl_region::Event,
179 _data: &(),
180 _conn: &Connection,
181 _qh: &QueueHandle<WaylandState>,
182 ) {
183 }
184}
185
186impl LayerShellHandler for WaylandState {
187 fn configure(
188 &mut self,
189 _conn: &Connection,
190 _qh: &QueueHandle<Self>,
191 layer: &LayerSurface,
192 _configure: LayerSurfaceConfigure,
193 _serial: u32,
194 ) {
195 layer.commit();
196 }
197
198 fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _layer: &LayerSurface) {}
199}
200
201impl ShmHandler for WaylandState {
202 fn shm_state(&mut self) -> &mut Shm {
203 &mut self.shm
204 }
205}
206
207impl OutputHandler for WaylandState {
208 fn output_state(&mut self) -> &mut OutputState {
209 &mut self.output_state
210 }
211 fn new_output(
212 &mut self,
213 _conn: &Connection,
214 _qh: &QueueHandle<Self>,
215 output: wl_output::WlOutput,
216 ) {
217 let id = output.id().protocol_id();
218 tracing::info!("Output detected: protocol_id={id}");
219
220 let mut info = OutputInfo {
221 name: format!("output-{id}"),
222 width: 1920,
223 height: 1080,
224 scale_factor: 1,
225 wl_output: output,
226 };
227
228 if let Some(info_data) = self.output_state.info(&info.wl_output) {
230 if let Some(mode) = info_data.modes.iter().find(|m| m.current) {
231 info.width = mode.dimensions.0 as u32;
232 info.height = mode.dimensions.1 as u32;
233 }
234 info.scale_factor = info_data.scale_factor;
235 let resolved = info_data
236 .name
237 .as_deref()
238 .filter(|n| !n.is_empty())
239 .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
240 if let Some(real_name) = resolved {
241 info.name = real_name.to_string();
242 } else if !info_data.make.is_empty() || !info_data.model.is_empty() {
243 let fallback = format!("{} {}", info_data.make, info_data.model)
244 .trim()
245 .to_string();
246 if !fallback.is_empty() {
247 info.name = fallback;
248 }
249 }
250 }
251
252 self.outputs.insert(id, info.clone());
253
254 if self.hotplug.is_some() {
258 let name = info.name.clone();
259 let renderer = self.hotplug.as_ref().unwrap().renderer.clone();
260 let display_ptr = self.hotplug.as_ref().unwrap().display_ptr.0;
261 let config = self.hotplug.as_ref().unwrap().config.clone();
262 let render_states = self.hotplug.as_ref().unwrap().render_states.clone();
263 match create_render_state_for_output_sync(
264 &renderer,
265 display_ptr,
266 self,
267 _qh,
268 &info,
269 &config,
270 ) {
271 Ok(rs) => {
272 let rs = std::sync::Arc::new(tokio::sync::Mutex::new(rs));
273 tokio::spawn(async move {
274 render_states.lock().await.insert(name.clone(), rs.clone());
275
276 let state_path = dirs::cache_dir()
278 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
279 .join(format!("wallr/last_wallpaper/{name}"));
280 if let Ok(path_str) = std::fs::read_to_string(&state_path) {
281 let p = std::path::Path::new(path_str.trim());
282 if p.exists() {
283 let mut lock = rs.lock().await;
284 let effect = crate::animation::Effect::Fade(
285 crate::animation::FadeParams::default(),
286 );
287 let _ = lock.set_wallpaper(p, &effect, 0, 0).await;
288 }
289 }
290 tracing::info!("Hotplug: output {name} ready");
291 });
292 }
293 Err(e) => {
294 tracing::error!("Hotplug: failed to create render state for {name}: {e}");
295 }
296 }
297 }
298 }
299 fn update_output(
300 &mut self,
301 _conn: &Connection,
302 _qh: &QueueHandle<Self>,
303 output: wl_output::WlOutput,
304 ) {
305 let id = output.id().protocol_id();
306 if let Some(info) = self.outputs.get_mut(&id) {
307 let mut new_width = info.width;
308 let mut new_height = info.height;
309 let mut new_scale = info.scale_factor;
310
311 if let Some(mode) = self
312 .output_state
313 .info(&output)
314 .and_then(|i| i.modes.iter().find(|m| m.current).cloned())
315 {
316 new_width = mode.dimensions.0 as u32;
317 new_height = mode.dimensions.1 as u32;
318 }
319 if let Some(info_data) = self.output_state.info(&output) {
320 new_scale = info_data.scale_factor;
321 let resolved = info_data
322 .name
323 .as_deref()
324 .filter(|n| !n.is_empty())
325 .or(info_data.description.as_deref().filter(|n| !n.is_empty()));
326 if let Some(real_name) = resolved {
327 if real_name != info.name {
328 tracing::info!(
329 "Output {id}: resolved name '{}' -> '{}'",
330 info.name,
331 real_name
332 );
333 info.name = real_name.to_string();
334 }
335 } else if info.name.starts_with("output-")
336 && (!info_data.make.is_empty() || !info_data.model.is_empty())
337 {
338 let fallback = format!("{} {}", info_data.make, info_data.model)
339 .trim()
340 .to_string();
341 if !fallback.is_empty() {
342 tracing::info!(
343 "Output {id}: fallback name '{}' -> '{}'",
344 info.name,
345 fallback
346 );
347 info.name = fallback;
348 }
349 }
350 }
351
352 let new_w = new_width * new_scale as u32;
353 let new_h = new_height * new_scale as u32;
354 let changed =
355 new_w != info.width || new_h != info.height || new_scale != info.scale_factor;
356
357 info.width = new_width;
358 info.height = new_height;
359 info.scale_factor = new_scale;
360
361 if changed {
363 let name = info.name.clone();
364 if let Some(ref hotplug) = self.hotplug {
365 let render_states = hotplug.render_states.clone();
366 let renderer = hotplug.renderer.clone();
367 tokio::spawn(async move {
368 let states = render_states.lock().await;
369 if let Some(rs) = states.get(&name) {
370 let mut lock = rs.lock().await;
371 lock.width = new_w;
372 lock.height = new_h;
373 let surf_config = wgpu::SurfaceConfiguration {
374 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
375 format: lock.format,
376 width: new_w,
377 height: new_h,
378 present_mode: wgpu::PresentMode::Fifo,
379 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
380 view_formats: vec![],
381 desired_maximum_frame_latency: 2,
382 };
383 lock.surface.configure(&renderer.device, &surf_config);
384 tracing::info!("Hotplug: reconfigured {name} to {new_w}x{new_h}");
385 }
386 });
387 }
388 }
389 }
390 }
391 fn output_destroyed(
392 &mut self,
393 _conn: &Connection,
394 _qh: &QueueHandle<Self>,
395 output: wl_output::WlOutput,
396 ) {
397 let id = output.id().protocol_id();
398 if let Some(info) = self.outputs.remove(&id) {
399 tracing::info!("Output disconnected: {} (protocol_id={})", info.name, id);
400 self.surfaces.retain(|(pid, _)| *pid != id);
402 if let Some(ref hotplug) = self.hotplug {
404 let name = info.name.clone();
405 let render_states = hotplug.render_states.clone();
406 tokio::spawn(async move {
407 if let Some(rs) = render_states.lock().await.remove(&name) {
408 let lock = rs.lock().await;
409 lock.video_playback.stop();
410 tracing::info!("Hotplug: cleaned up render state for {name}");
411 }
412 });
413 }
414 }
415 }
416}
417
418delegate_compositor!(WaylandState);
419delegate_layer!(WaylandState);
420delegate_output!(WaylandState);
421delegate_registry!(WaylandState);
422delegate_shm!(WaylandState);
423
424struct LivePacer {
426 lock: std::sync::Mutex<()>,
427 cond: std::sync::Condvar,
428}
429
430impl LivePacer {
431 fn new() -> Self {
432 Self {
433 lock: std::sync::Mutex::new(()),
434 cond: std::sync::Condvar::new(),
435 }
436 }
437
438 fn notify(&self) {
439 let _guard = self.lock.lock().unwrap();
440 self.cond.notify_all();
441 }
442
443 fn wait_until(&self, deadline: std::time::Instant) {
446 let guard = self.lock.lock().unwrap();
447 let now = std::time::Instant::now();
448 if deadline <= now {
449 return;
450 }
451 let _ = self
452 .cond
453 .wait_timeout_while(guard, deadline - now, |_| true);
454 }
455}
456
457struct RenderState {
458 renderer: std::sync::Arc<Renderer>,
459 surface: &'static wgpu::Surface<'static>,
460 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
464 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
467 pacer: std::sync::Arc<LivePacer>,
470 current_bind: Option<wgpu::BindGroup>,
471 current_tex: Option<wgpu::Texture>,
472 width: u32,
473 height: u32,
474 current_width: u32,
475 current_height: u32,
476 format: wgpu::TextureFormat,
477 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
479 hw_accel: crate::video::HwAccel,
481 scaling_mode: u32,
483 per_output_uniforms: std::sync::Arc<crate::renderer::PerOutputUniforms>,
485 last_wallpaper: Option<std::path::PathBuf>,
487 pre_blank: Option<(std::path::PathBuf, u32)>,
489 blanked: bool,
491 gif_paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
493}
494
495struct CommitData {
498 bg_bind: wgpu::BindGroup,
499 new_bind: wgpu::BindGroup,
500 img_width: u32,
501 img_height: u32,
502 old_img_width: u32,
503 old_img_height: u32,
504 format: wgpu::TextureFormat,
505 width: u32,
506 height: u32,
507 animated: Option<crate::animated::AnimatedImage>,
510 is_video: bool,
512 generation: u64,
515 scaling_mode: u32,
517}
518
519impl RenderState {
520 async fn set_wallpaper(
521 &mut self,
522 path: &std::path::Path,
523 effect: &crate::animation::Effect,
524 duration_ms: u32,
525 scaling_mode: u32,
526 ) -> anyhow::Result<()> {
527 self.scaling_mode = scaling_mode;
528 let commit = self.commit_wallpaper(path, scaling_mode)?;
529 self.spawn_transition(commit, effect, duration_ms);
530 self.last_wallpaper = Some(path.to_path_buf());
532 Ok(())
533 }
534
535 fn commit_wallpaper(
539 &mut self,
540 path: &std::path::Path,
541 scaling_mode: u32,
542 ) -> anyhow::Result<CommitData> {
543 use image::ImageReader;
544
545 if crate::video::VideoDecoder::is_video_file(path) {
547 tracing::info!("Video file detected: {:?}", path);
548
549 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
555 self.pacer.notify();
556
557 let metadata = self.video_playback.start(path, self.hw_accel)?;
560
561 let first_frame = self
564 .video_playback
565 .wait_first_frame(std::time::Duration::from_millis(1000));
566
567 let (new_tex, new_bind, img_width, img_height) = if let Some(frame) = first_frame {
568 let (tex, bind) = self.renderer.create_texture(frame.width, frame.height);
569 self.renderer
570 .update_texture(&tex, &frame.data, frame.width, frame.height);
571 (tex, bind, frame.width, frame.height)
572 } else {
573 tracing::warn!("No first frame available, using black texture");
575 let (tex, bind) = self
576 .renderer
577 .create_texture(metadata.width, metadata.height);
578 let black = vec![0u8; (metadata.width * metadata.height * 4) as usize];
579 self.renderer
580 .update_texture(&tex, &black, metadata.width, metadata.height);
581 (tex, bind, metadata.width, metadata.height)
582 };
583
584 let old_bind = self.current_bind.take();
585 let (old_img_width, old_img_height) = if old_bind.is_some() {
586 (self.current_width.max(1), self.current_height.max(1))
587 } else {
588 (img_width, img_height)
589 };
590 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
591
592 drop(self.current_tex.take());
593 self.current_tex = Some(new_tex);
594 self.current_bind = Some(new_bind.clone());
595 self.current_width = img_width;
596 self.current_height = img_height;
597
598 return Ok(CommitData {
599 bg_bind,
600 new_bind,
601 img_width,
602 img_height,
603 old_img_width,
604 old_img_height,
605 format: self.format,
606 width: self.width,
607 height: self.height,
608 animated: None,
609 is_video: true,
610 generation,
611 scaling_mode,
612 });
613 }
614
615 self.video_playback.stop();
619
620 let mut animated = crate::animated::AnimatedImage::decode(path)?;
623 let (new_tex, new_bind, img_width, img_height) = if let Some(anim) = animated.as_mut() {
624 let (w, h) = (anim.width, anim.height);
625 let (tex, bind) = self.renderer.create_texture(w, h);
626 let first = anim.first_frame();
627 if !first.is_empty() {
628 self.renderer.update_texture(&tex, first, w, h);
629 }
630 (tex, bind, w, h)
631 } else {
632 let new_img = ImageReader::open(path)?.decode()?;
633 let (tex, bind) = self.renderer.load_texture(&new_img)?;
634 (tex, bind, new_img.width(), new_img.height())
635 };
636
637 let old_bind = self.current_bind.take();
638 let (old_img_width, old_img_height) = if old_bind.is_some() {
639 (self.current_width.max(1), self.current_height.max(1))
640 } else {
641 (img_width, img_height)
642 };
643 let bg_bind = old_bind.unwrap_or_else(|| new_bind.clone());
648
649 drop(self.current_tex.take());
650 self.current_tex = Some(new_tex);
651 self.current_bind = Some(new_bind.clone());
652 self.current_width = img_width;
653 self.current_height = img_height;
654
655 let generation = self.playback_gen.fetch_add(1, Ordering::SeqCst) + 1;
656 self.pacer.notify();
657
658 Ok(CommitData {
659 bg_bind,
660 new_bind,
661 img_width,
662 img_height,
663 old_img_width,
664 old_img_height,
665 format: self.format,
666 width: self.width,
667 height: self.height,
668 animated,
669 is_video: false,
670 generation,
671 scaling_mode,
672 })
673 }
674
675 fn spawn_transition(
680 &self,
681 commit: CommitData,
682 effect: &crate::animation::Effect,
683 duration_ms: u32,
684 ) {
685 let renderer = self.renderer.clone();
686 let surface: &'static wgpu::Surface<'static> = self.surface;
687 let render_lock = self.render_lock.clone();
688 let playback_gen = self.playback_gen.clone();
689 let pacer = self.pacer.clone();
690 let video_playback = self.video_playback.clone();
691 let per_output_uniforms = std::sync::Arc::clone(&self.per_output_uniforms);
692 let gif_paused = self.gif_paused.clone();
693 let effect = effect.clone();
694 drop(tokio::task::spawn_blocking(move || {
695 render_transition(
696 renderer,
697 surface,
698 render_lock,
699 playback_gen,
700 pacer,
701 video_playback,
702 gif_paused,
703 commit,
704 effect,
705 duration_ms,
706 &per_output_uniforms,
707 );
708 }));
709 }
710}
711
712#[allow(clippy::too_many_arguments)]
720fn render_transition(
721 renderer: std::sync::Arc<Renderer>,
722 surface: &'static wgpu::Surface<'static>,
723 render_lock: std::sync::Arc<std::sync::Mutex<()>>,
724 playback_gen: std::sync::Arc<std::sync::atomic::AtomicU64>,
725 pacer: std::sync::Arc<LivePacer>,
726 video_playback: std::sync::Arc<crate::video::VideoPlayback>,
727 gif_paused: std::sync::Arc<std::sync::atomic::AtomicBool>,
728 mut commit: CommitData,
729 effect: crate::animation::Effect,
730 duration_ms: u32,
731 per_output_uniforms: &crate::renderer::PerOutputUniforms,
732) {
733 let _guard = render_lock
734 .lock()
735 .unwrap_or_else(|poisoned| poisoned.into_inner());
736
737 let duration = std::time::Duration::from_millis(u64::from(duration_ms.max(1)));
738 let start = std::time::Instant::now();
739 loop {
740 let progress = start.elapsed().as_secs_f32() / duration.as_secs_f32();
741 let uniforms = crate::animation::compute_effect_uniforms(&effect, progress.clamp(0.0, 1.0));
742 let status = renderer.render_frame(
743 crate::renderer::FrameRequest {
744 surface,
745 format: commit.format,
746 bg_bind: &commit.bg_bind,
747 new_bind: &commit.new_bind,
748 effect: &uniforms,
749 width: commit.width,
750 height: commit.height,
751 img_width: commit.img_width,
752 img_height: commit.img_height,
753 old_img_width: commit.old_img_width,
754 old_img_height: commit.old_img_height,
755 scaling_mode: commit.scaling_mode,
756 },
757 per_output_uniforms,
758 );
759 let status = match status {
760 Ok(status) => status,
761 Err(err) => {
762 eprintln!("wallr: transition render failed: {err}");
763 break;
764 }
765 };
766 if progress >= 1.0 || status == crate::renderer::FrameStatus::TimedOut {
767 break;
768 }
769 }
770
771 let mut animated = commit.animated.take();
775 if let Some(animated) = animated.as_mut()
776 && playback_gen.load(Ordering::SeqCst) == commit.generation
777 {
778 play_live(
779 &renderer,
780 surface,
781 &commit,
782 animated,
783 &playback_gen,
784 &pacer,
785 &gif_paused,
786 per_output_uniforms,
787 );
788 } else if commit.is_video && playback_gen.load(Ordering::SeqCst) == commit.generation {
789 play_video(
790 &renderer,
791 surface,
792 &commit,
793 &video_playback,
794 &playback_gen,
795 per_output_uniforms,
796 );
797 }
798}
799
800fn play_live(
806 renderer: &Renderer,
807 surface: &'static wgpu::Surface<'static>,
808 commit: &CommitData,
809 animated: &mut crate::animated::AnimatedImage,
810 playback_gen: &std::sync::atomic::AtomicU64,
811 pacer: &LivePacer,
812 gif_paused: &std::sync::Arc<std::sync::atomic::AtomicBool>,
813 per_output_uniforms: &crate::renderer::PerOutputUniforms,
814) {
815 let (tex_a, bind_a) = renderer.create_texture(animated.width, animated.height);
816 let (tex_b, bind_b) = renderer.create_texture(animated.width, animated.height);
817 let (frame_w, frame_h) = (animated.width, animated.height);
818 let (bytes_per_row, rows) = (frame_w * 4, frame_h);
819 let frame_bytes = bytes_per_row as u64 * rows as u64;
820
821 let direct_upload = bytes_per_row % 256 == 0;
824 let staging: Vec<wgpu::Buffer> = if direct_upload {
825 (0..2)
826 .map(|_| {
827 renderer.device.create_buffer(&wgpu::BufferDescriptor {
828 label: Some("wallr-gif-staging"),
829 size: frame_bytes,
830 usage: wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
831 mapped_at_creation: false,
832 })
833 })
834 .collect()
835 } else {
836 Vec::new()
837 };
838
839 let first = animated.first_frame();
840 if !first.is_empty() {
841 renderer.update_texture(&tex_a, first, frame_w, frame_h);
842 renderer.update_texture(&tex_b, first, frame_w, frame_h);
843 }
844 let binds = [bind_a, bind_b];
845 let textures = [tex_a, tex_b];
846
847 let upload = |renderer: &Renderer,
850 tgt: usize,
851 index: usize,
852 slot: usize,
853 animated: &mut crate::animated::AnimatedImage|
854 -> bool {
855 if direct_upload {
856 let buffer = &staging[slot];
857 let slice = buffer.slice(..);
858 slice.map_async(wgpu::MapMode::Write, |_| {});
859 renderer.device.poll(wgpu::Maintain::Wait);
860 let ok = {
861 let mut mapped = slice.get_mapped_range_mut();
862 animated.decompress_into(index, &mut mapped)
863 };
864 buffer.unmap();
865 if ok {
866 let mut encoder = renderer
867 .device
868 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
869 encoder.copy_buffer_to_texture(
870 wgpu::TexelCopyBufferInfo {
871 buffer,
872 layout: wgpu::TexelCopyBufferLayout {
873 offset: 0,
874 bytes_per_row: Some(bytes_per_row),
875 rows_per_image: Some(rows),
876 },
877 },
878 wgpu::TexelCopyTextureInfo {
879 texture: &textures[tgt],
880 mip_level: 0,
881 origin: wgpu::Origin3d::ZERO,
882 aspect: wgpu::TextureAspect::All,
883 },
884 wgpu::Extent3d {
885 width: frame_w,
886 height: frame_h,
887 depth_or_array_layers: 1,
888 },
889 );
890 renderer.queue.submit([encoder.finish()]);
891 return true;
892 }
893 } else {
894 let frame = animated.frame_at(index);
895 if !frame.is_empty() {
896 renderer.update_texture(&textures[tgt], frame, frame_w, frame_h);
897 return true;
898 }
899 }
900 false
901 };
902
903 let mut cur = 0usize; let mut cur_frame = 0usize; let mut next_frame = 0usize; let mut slot = 0usize; let start = std::time::Instant::now();
908 let mut paused_elapsed = std::time::Duration::ZERO; let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
910 loop {
911 if playback_gen.load(Ordering::SeqCst) != commit.generation {
912 return;
913 }
914
915 if gif_paused.load(Ordering::SeqCst) {
917 let pause_start = std::time::Instant::now();
918 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
920 let status = renderer.render_frame(
921 crate::renderer::FrameRequest {
922 surface,
923 format: commit.format,
924 bg_bind: &binds[cur],
925 new_bind: &binds[cur],
926 effect: &uniforms,
927 width: commit.width,
928 height: commit.height,
929 img_width: animated.width,
930 img_height: animated.height,
931 old_img_width: animated.width,
932 old_img_height: animated.height,
933 scaling_mode: commit.scaling_mode,
934 },
935 per_output_uniforms,
936 );
937 match status {
938 Ok(crate::renderer::FrameStatus::Presented) => {}
939 _ => return,
940 }
941 std::thread::sleep(std::time::Duration::from_millis(16));
943 paused_elapsed += pause_start.elapsed();
944 continue;
945 }
946
947 let index = animated.frame_index_at(start.elapsed() - paused_elapsed);
948 if index != cur_frame {
949 if next_frame != index {
950 upload(renderer, cur ^ 1, index, slot, animated);
951 slot ^= 1;
952 next_frame = index;
953 }
954 cur ^= 1;
955 cur_frame = index;
956 }
957 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
958 let status = renderer.render_frame(
959 crate::renderer::FrameRequest {
960 surface,
961 format: commit.format,
962 bg_bind: &binds[cur],
963 new_bind: &binds[cur],
964 effect: &uniforms,
965 width: commit.width,
966 height: commit.height,
967 img_width: animated.width,
968 img_height: animated.height,
969 old_img_width: animated.width,
970 old_img_height: animated.height,
971 scaling_mode: commit.scaling_mode,
972 },
973 per_output_uniforms,
974 );
975 match status {
976 Ok(crate::renderer::FrameStatus::Presented) => {}
977 _ => return,
978 }
979
980 let elapsed = start.elapsed() - paused_elapsed;
988 let total: std::time::Duration = animated.total_duration();
989 let loops = (elapsed.as_millis() / total.as_millis().max(1)) as u64;
990 let next_change = animated.frame_start(index + 1) + total * (loops as u32);
991 let wait = next_change.saturating_sub(elapsed);
992 if wait > std::time::Duration::ZERO {
993 let next = index + 1;
994 if next_frame != next {
995 upload(renderer, cur ^ 1, next, slot, animated);
996 slot ^= 1;
997 next_frame = next;
998 }
999 pacer.wait_until(std::time::Instant::now() + wait);
1000 }
1001 }
1002}
1003
1004fn play_video(
1006 renderer: &Renderer,
1007 surface: &'static wgpu::Surface<'static>,
1008 commit: &CommitData,
1009 video_playback: &std::sync::Arc<crate::video::VideoPlayback>,
1010 playback_gen: &std::sync::atomic::AtomicU64,
1011 per_output_uniforms: &crate::renderer::PerOutputUniforms,
1012) {
1013 let (width, height) = match video_playback.metadata() {
1015 Some(meta) => (meta.width, meta.height),
1016 None => {
1017 tracing::warn!("No video metadata available");
1018 return;
1019 }
1020 };
1021
1022 let (texture, bind) = renderer.create_texture(width, height);
1023 let static_effect = crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1024
1025 let mut uploaded = false;
1028
1029 loop {
1030 if playback_gen.load(Ordering::SeqCst) != commit.generation {
1035 return;
1036 }
1037
1038 if let Some(frame) = video_playback.next_frame() {
1041 if frame.width != width || frame.height != height {
1044 continue;
1045 }
1046 renderer.update_texture(&texture, &frame.data, frame.width, frame.height);
1047 uploaded = true;
1048 }
1049
1050 if !uploaded {
1051 std::thread::sleep(std::time::Duration::from_millis(2));
1054 continue;
1055 }
1056
1057 let uniforms = crate::animation::compute_effect_uniforms(&static_effect, 1.0);
1058 let status = renderer.render_frame(
1059 crate::renderer::FrameRequest {
1060 surface,
1061 format: commit.format,
1062 bg_bind: &bind,
1063 new_bind: &bind,
1064 effect: &uniforms,
1065 width: commit.width,
1066 height: commit.height,
1067 img_width: width,
1068 img_height: height,
1069 old_img_width: width,
1070 old_img_height: height,
1071 scaling_mode: commit.scaling_mode,
1072 },
1073 per_output_uniforms,
1074 );
1075
1076 match status {
1077 Ok(crate::renderer::FrameStatus::Presented) => {}
1078 other => {
1081 tracing::warn!("Video present failed ({:?}), stopping playback", other);
1082 video_playback.stop();
1083 return;
1084 }
1085 }
1086 }
1087}
1088
1089async fn resolve_targets(
1094 render_states: &std::collections::HashMap<
1095 String,
1096 std::sync::Arc<tokio::sync::Mutex<RenderState>>,
1097 >,
1098 monitor: Option<&str>,
1099) -> Vec<std::sync::Arc<tokio::sync::Mutex<RenderState>>> {
1100 match monitor {
1101 Some(name) => {
1102 if let Some(rs) = render_states.get(name) {
1103 vec![rs.clone()]
1104 } else {
1105 Vec::new()
1106 }
1107 }
1108 None => render_states.values().cloned().collect(),
1109 }
1110}
1111
1112pub struct Daemon {
1113 config: WallrConfig,
1114 paused: Arc<AtomicBool>,
1115 engine: Arc<Mutex<WallpaperEngine>>,
1116}
1117
1118impl Daemon {
1119 pub fn new(config: WallrConfig) -> Result<Self, DaemonError> {
1120 let engine = WallpaperEngine::new(config.clone())?;
1121 Ok(Self {
1122 config,
1123 paused: Arc::new(AtomicBool::new(false)),
1124 engine: Arc::new(Mutex::new(engine)),
1125 })
1126 }
1127
1128 pub async fn start(self) -> Result<(), DaemonError> {
1129 let socket_path = crate::config::expand_path(&self.config.daemon.socket);
1130 if socket_path.exists() {
1131 if tokio::net::UnixStream::connect(&socket_path).await.is_ok() {
1132 return Err(DaemonError::AlreadyRunning(
1133 socket_path.to_string_lossy().to_string(),
1134 ));
1135 }
1136 let _ = std::fs::remove_file(&socket_path);
1137 }
1138
1139 let renderer = Renderer::new()
1140 .await
1141 .map_err(|e| DaemonError::StartError(format!("GPU init failed: {e}")))?;
1142
1143 let conn = Connection::connect_to_env()
1144 .map_err(|e| DaemonError::StartError(format!("Failed to connect to Wayland: {e:?}")))?;
1145 let backend = conn.backend();
1146 let display_ptr = backend.display_ptr() as *mut std::ffi::c_void;
1147
1148 let (globals, mut event_queue) = registry_queue_init(&conn)
1149 .map_err(|e| DaemonError::StartError(format!("registry_queue_init failed: {e:?}")))?;
1150 let qh = event_queue.handle();
1151
1152 let compositor_state = CompositorState::bind(&globals, &qh)
1153 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
1154 let layer_shell = LayerShell::bind(&globals, &qh)
1155 .map_err(|e| DaemonError::StartError(format!("layer_shell bind failed: {e:?}")))?;
1156 let shm = Shm::bind(&globals, &qh)
1157 .map_err(|e| DaemonError::StartError(format!("shm bind failed: {e:?}")))?;
1158
1159 let compositor = globals
1161 .bind::<wl_compositor::WlCompositor, WaylandState, smithay_client_toolkit::globals::GlobalData>(
1162 &qh,
1163 1..=4,
1164 smithay_client_toolkit::globals::GlobalData,
1165 )
1166 .map_err(|e| DaemonError::StartError(format!("compositor bind failed: {e:?}")))?;
1167
1168 let mut wayland_state = WaylandState {
1169 registry_state: RegistryState::new(&globals),
1170 output_state: OutputState::new(&globals, &qh),
1171 compositor_state,
1172 shm,
1173 outputs: std::collections::HashMap::new(),
1174 surfaces: Vec::new(),
1175 layer_shell,
1176 compositor,
1177 hotplug: None,
1178 };
1179
1180 for i in 0..5 {
1184 event_queue
1185 .roundtrip(&mut wayland_state)
1186 .map_err(|e| DaemonError::StartError(format!("roundtrip {i} failed: {e:?}")))?;
1187 }
1188
1189 if wayland_state.outputs.is_empty() {
1190 return Err(DaemonError::StartError(
1191 "no outputs detected after roundtrip".into(),
1192 ));
1193 }
1194
1195 tracing::info!(
1196 "Detected {} output(s): {:?}",
1197 wayland_state.outputs.len(),
1198 wayland_state
1199 .outputs
1200 .values()
1201 .map(|o| format!("{} ({}x{})", o.name, o.width, o.height))
1202 .collect::<Vec<_>>()
1203 );
1204
1205 let renderer = std::sync::Arc::new(renderer);
1206
1207 let mut render_states_map: std::collections::HashMap<String, Arc<Mutex<RenderState>>> =
1211 std::collections::HashMap::new();
1212
1213 let output_info: Vec<(u32, OutputInfo)> = wayland_state
1216 .outputs
1217 .iter()
1218 .map(|(k, v)| {
1219 (
1220 *k,
1221 OutputInfo {
1222 name: v.name.clone(),
1223 width: v.width,
1224 height: v.height,
1225 scale_factor: v.scale_factor,
1226 wl_output: v.wl_output.clone(),
1227 },
1228 )
1229 })
1230 .collect();
1231
1232 for (proto_id, info) in &output_info {
1233 let name = info.name.clone();
1234 let rs = Self::create_render_state_for_output(
1235 &renderer,
1236 display_ptr,
1237 &mut wayland_state,
1238 &qh,
1239 info,
1240 &self.config,
1241 )
1242 .await?;
1243 let rs = Arc::new(Mutex::new(rs));
1244 render_states_map.insert(name.clone(), rs.clone());
1245
1246 let state_path = dirs::cache_dir()
1248 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1249 .join(format!("wallr/last_wallpaper/{name}"));
1250 if let Ok(path_str) = std::fs::read_to_string(&state_path) {
1251 let p = std::path::Path::new(path_str.trim());
1252 if p.exists() {
1253 let mut lock = rs.lock().await;
1254 let effect =
1255 crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1256 let _ = lock.set_wallpaper(p, &effect, 0, 0).await;
1257 }
1258 }
1259
1260 tracing::info!("Output ready: {name} ({proto_id})");
1261 }
1262
1263 let render_states: std::sync::Arc<
1266 tokio::sync::Mutex<std::collections::HashMap<String, Arc<Mutex<RenderState>>>>,
1267 > = std::sync::Arc::new(tokio::sync::Mutex::new(render_states_map));
1268
1269 wayland_state.hotplug = Some(DaemonHotplug {
1272 renderer: renderer.clone(),
1273 config: self.config.clone(),
1274 display_ptr: SendDisplayPtr(display_ptr),
1275 render_states: render_states.clone(),
1276 });
1277
1278 event_queue
1281 .roundtrip(&mut wayland_state)
1282 .map_err(|e| DaemonError::StartError(format!("hotplug roundtrip failed: {e:?}")))?;
1283
1284 let paused_clone = self.paused.clone();
1285 let engine_clone = self.engine.clone();
1286 let render_states_clone = render_states.clone();
1287
1288 {
1292 let rs_map = render_states.clone();
1293 let socket_path = socket_path.clone();
1294 tokio::spawn(async move {
1295 use tokio::signal::unix::{SignalKind, signal};
1296 let mut term = signal(SignalKind::terminate()).expect("SIGTERM handler");
1297 let mut int = signal(SignalKind::interrupt()).expect("SIGINT handler");
1298 let mut hup = signal(SignalKind::hangup()).expect("SIGHUP handler");
1299 tokio::select! {
1300 _ = term.recv() => {}
1301 _ = int.recv() => {}
1302 _ = hup.recv() => {}
1303 }
1304 tracing::info!("Signal received, shutting down gracefully");
1305 let states = rs_map.lock().await;
1306 for rs in states.values() {
1307 if let Ok(state) = rs.try_lock() {
1308 state.video_playback.stop();
1309 }
1310 }
1311 drop(states);
1312 let _ = std::fs::remove_file(&socket_path);
1313 std::process::exit(0);
1314 });
1315 }
1316
1317 let ipc_socket_path = socket_path.clone();
1318 start_ipc_server(&socket_path, move |cmd| {
1319 let paused = paused_clone.clone();
1320 let engine = engine_clone.clone();
1321 let render_states = render_states_clone.clone();
1322 let stop_socket = ipc_socket_path.clone();
1323 async move {
1324 let render_states = render_states.lock().await;
1325 match cmd {
1326 IpcCommand::Pause { monitor } => {
1327 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1328 if targets.is_empty() {
1329 return IpcResponse {
1330 success: false,
1331 message: Some("No matching outputs".into()),
1332 };
1333 }
1334 if monitor.is_none() {
1335 paused.store(true, Ordering::SeqCst);
1336 }
1337 for rs in targets {
1338 let rs_lock = rs.lock().await;
1339 rs_lock.video_playback.pause();
1340 rs_lock.gif_paused.store(true, Ordering::SeqCst);
1341 }
1342 IpcResponse {
1343 success: true,
1344 message: Some("Paused".into()),
1345 }
1346 }
1347 IpcCommand::Resume { monitor } => {
1348 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1349 if targets.is_empty() {
1350 return IpcResponse {
1351 success: false,
1352 message: Some("No matching outputs".into()),
1353 };
1354 }
1355 if monitor.is_none() {
1356 paused.store(false, Ordering::SeqCst);
1357 }
1358 for rs in targets {
1359 let rs_lock = rs.lock().await;
1360 rs_lock.video_playback.resume();
1361 rs_lock.gif_paused.store(false, Ordering::SeqCst);
1362 }
1363 IpcResponse {
1364 success: true,
1365 message: Some("Resumed".into()),
1366 }
1367 }
1368 IpcCommand::Reload => {
1369 let lock = engine.lock().await;
1370 match lock.reload() {
1371 Ok(_) => IpcResponse {
1372 success: true,
1373 message: Some("Reloaded".into()),
1374 },
1375 Err(e) => IpcResponse {
1376 success: false,
1377 message: Some(e.to_string()),
1378 },
1379 }
1380 }
1381 IpcCommand::Preview {
1382 path,
1383 effect,
1384 duration_ms,
1385 no_theme,
1386 theme_override,
1387 monitor,
1388 scaling_mode,
1389 } => {
1390 if paused.load(Ordering::SeqCst) {
1391 return IpcResponse {
1392 success: false,
1393 message: Some("Daemon is paused".into()),
1394 };
1395 }
1396 let p = std::path::PathBuf::from(&path);
1397 if !p.exists() {
1398 return IpcResponse {
1399 success: false,
1400 message: Some(format!("File not found: {}", path)),
1401 };
1402 }
1403
1404 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1406 if targets.is_empty() {
1407 return IpcResponse {
1408 success: false,
1409 message: match &monitor {
1410 Some(name) => Some(format!("Unknown monitor: {name}")),
1411 None => Some("No outputs available".into()),
1412 },
1413 };
1414 }
1415
1416 {
1418 let persist_names: Vec<&str> = match &monitor {
1419 Some(name) => vec![name.as_str()],
1420 None => render_states.keys().map(|s| s.as_str()).collect(),
1421 };
1422 for name in persist_names {
1423 let state_path = dirs::cache_dir()
1424 .unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
1425 .join(format!("wallr/last_wallpaper/{name}"));
1426 if let Some(parent) = state_path.parent() {
1427 let _ = std::fs::create_dir_all(parent);
1428 }
1429 let _ = std::fs::write(&state_path, &path);
1430 }
1431 }
1432
1433 let effect = effect.unwrap_or_else(|| {
1434 crate::animation::Effect::Fade(crate::animation::FadeParams::default())
1435 });
1436 let is_video = crate::video::VideoDecoder::is_video_file(&p);
1440 let duration = duration_ms.unwrap_or(if is_video { 150 } else { 2000 });
1441 let sm = scaling_mode.unwrap_or(crate::config::ScalingMode::Fill);
1442 let scaling_mode_u32 = match sm {
1443 crate::config::ScalingMode::Fill => 0u32,
1444 crate::config::ScalingMode::Fit => 1,
1445 crate::config::ScalingMode::Stretch => 2,
1446 crate::config::ScalingMode::Center => 3,
1447 crate::config::ScalingMode::Tile => 4,
1448 };
1449
1450 let mut last_err = None;
1451 for rs in &targets {
1452 let rs_clone = rs.clone();
1453 let p_clone = p.clone();
1454 let effect_clone = effect.clone();
1455 let result = tokio::task::spawn_blocking(move || {
1456 let rt = tokio::runtime::Handle::current();
1457 rt.block_on(async {
1458 let mut lock = rs_clone.lock().await;
1459 lock.set_wallpaper(
1460 &p_clone,
1461 &effect_clone,
1462 duration,
1463 scaling_mode_u32,
1464 )
1465 .await
1466 })
1467 })
1468 .await;
1469
1470 if let Err(e) = result {
1471 last_err = Some(format!("Task spawn failed: {e}"));
1472 continue;
1473 }
1474 if let Err(e) = result.unwrap() {
1475 last_err = Some(format!("Render failed: {e}"));
1476 }
1477 }
1478
1479 match last_err {
1480 Some(e) => IpcResponse {
1481 success: false,
1482 message: Some(e),
1483 },
1484 None => {
1485 let opts = SetOptions {
1486 no_theme,
1487 theme_provider: theme_override,
1488 monitor,
1489 };
1490 let mut eng = engine.lock().await;
1491 match eng.set_wallpaper(&p, &opts).await {
1492 Ok(()) => IpcResponse {
1493 success: true,
1494 message: None,
1495 },
1496 Err(e) => IpcResponse {
1497 success: true,
1498 message: Some(format!(
1499 "Wallpaper set, but hooks/theme failed: {e}"
1500 )),
1501 },
1502 }
1503 }
1504 }
1505 }
1506 IpcCommand::Stop => {
1507 let sp = stop_socket.clone();
1508 tokio::spawn(async move {
1509 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
1510 let _ = std::fs::remove_file(&sp);
1511 std::process::exit(0);
1512 });
1513 IpcResponse {
1514 success: true,
1515 message: Some("Stopping".into()),
1516 }
1517 }
1518 IpcCommand::Status => {
1519 let state = if paused.load(Ordering::SeqCst) {
1520 "paused"
1521 } else {
1522 "running"
1523 };
1524 IpcResponse {
1525 success: true,
1526 message: Some(format!("wallr daemon {}", state)),
1527 }
1528 }
1529 IpcCommand::Seek {
1530 timestamp_ms,
1531 monitor,
1532 } => {
1533 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1534 if targets.is_empty() {
1535 return IpcResponse {
1536 success: false,
1537 message: match &monitor {
1538 Some(name) => Some(format!("Unknown monitor: {name}")),
1539 None => Some("No outputs available".into()),
1540 },
1541 };
1542 }
1543 let mut seek_count = 0u32;
1545 let mut errors = Vec::new();
1546 for (name, rs) in render_states.iter() {
1547 if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
1548 continue;
1549 }
1550 let rs_lock = rs.lock().await;
1551 match rs_lock
1552 .video_playback
1553 .seek(std::time::Duration::from_millis(timestamp_ms))
1554 {
1555 Ok(()) => {
1556 seek_count += 1;
1557 }
1558 Err(e) => {
1559 errors.push(format!("{}: {}", name, e));
1560 }
1561 }
1562 }
1563 if seek_count == 0 {
1564 IpcResponse {
1565 success: false,
1566 message: Some(format!("Seek failed on all outputs: {}", errors.join("; "))),
1567 }
1568 } else if !errors.is_empty() {
1569 IpcResponse {
1570 success: true,
1571 message: Some(format!(
1572 "Seeked {} output(s) to {}ms, {} failed: {}",
1573 seek_count,
1574 timestamp_ms,
1575 errors.len(),
1576 errors.join("; ")
1577 )),
1578 }
1579 } else {
1580 IpcResponse {
1581 success: true,
1582 message: Some(format!("Seeked {} output(s) to {}ms", seek_count, timestamp_ms)),
1583 }
1584 }
1585 }
1586 IpcCommand::Info { monitor } => {
1587 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1588 if targets.is_empty() {
1589 return IpcResponse {
1590 success: false,
1591 message: match &monitor {
1592 Some(name) => Some(format!("Unknown monitor: {name}")),
1593 None => Some("No outputs available".into()),
1594 },
1595 };
1596 }
1597
1598 let mut lines = vec![
1599 format!("wallr v{}", env!("CARGO_PKG_VERSION")),
1600 String::new(),
1601 format!("Outputs: {}", render_states.len()),
1602 ];
1603 for name in render_states.keys() {
1604 lines.push(format!(" - {name}"));
1605 }
1606
1607 for (name, rs) in render_states.iter() {
1609 if monitor.is_some() && monitor.as_deref() != Some(name.as_str()) {
1610 continue;
1611 }
1612 let rs_lock = rs.lock().await;
1613 let gpu_info =
1614 crate::video::gpu::adapter_diagnostics(&rs_lock.renderer.adapter);
1615
1616 lines.push(String::new());
1617 lines.push(format!("[{name}] {}x{}", rs_lock.width, rs_lock.height));
1618 lines.push(gpu_info);
1619
1620 match rs_lock.video_playback.metadata() {
1621 Some(meta) => {
1622 let decoder_info = rs_lock.video_playback.decoder_info();
1623 let hw = rs_lock.video_playback.hw_accel_in_use();
1624 let state = if rs_lock.video_playback.is_paused() {
1625 "paused"
1626 } else {
1627 "playing"
1628 };
1629 let position = rs_lock
1630 .video_playback
1631 .position()
1632 .map(|p| format!("{:.2}s", p.as_secs_f64()))
1633 .unwrap_or_else(|| "?".to_string());
1634 lines.push(String::new());
1635 lines.push("Video:".into());
1636 lines.push(format!(
1637 " Resolution: {}x{}",
1638 meta.width, meta.height
1639 ));
1640 lines.push(format!(" FPS: {:.2}", meta.fps));
1641 lines.push(format!(
1642 " Duration: {:.2}s",
1643 meta.duration.as_secs_f64()
1644 ));
1645 lines.push(format!(
1646 " Codec: {}",
1647 decoder_info
1648 .as_ref()
1649 .map(|d| d.codec_name.as_str())
1650 .unwrap_or("unknown")
1651 ));
1652 lines.push(format!(" Container: {}", meta.format));
1653 lines.push(format!(" Decoder: {}", hw.name()));
1654 lines.push(format!(
1655 " GPU Decode: {}",
1656 if hw == crate::video::HwAccel::Software {
1657 "disabled"
1658 } else {
1659 "enabled"
1660 }
1661 ));
1662 lines.push(format!(" State: {} @ {}", state, position));
1663 }
1664 None => {
1665 lines.push(String::new());
1666 lines.push("Video: none active".into());
1667 lines.push("Decoder: idle".into());
1668 }
1669 }
1670 }
1671
1672 IpcResponse {
1673 success: true,
1674 message: Some(lines.join("\n")),
1675 }
1676 }
1677 IpcCommand::MonitorList => {
1678 let mut lines = Vec::new();
1679 for (name, rs) in render_states.iter() {
1680 let lock = rs.lock().await;
1681 lines.push(format!("{}: {}x{}", name, lock.width, lock.height));
1682 }
1683 if lines.is_empty() {
1684 IpcResponse {
1685 success: true,
1686 message: Some("No monitors connected".into()),
1687 }
1688 } else {
1689 IpcResponse {
1690 success: true,
1691 message: Some(lines.join("\n")),
1692 }
1693 }
1694 }
1695 IpcCommand::MonitorCurrent => {
1696 if let Some((name, rs)) = render_states.iter().next() {
1698 let lock = rs.lock().await;
1699 IpcResponse {
1700 success: true,
1701 message: Some(format!("{}: {}x{}", name, lock.width, lock.height)),
1702 }
1703 } else {
1704 IpcResponse {
1705 success: false,
1706 message: Some("No monitors connected".into()),
1707 }
1708 }
1709 }
1710 IpcCommand::Blank { monitor } => {
1711 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1712 if targets.is_empty() {
1713 return IpcResponse {
1714 success: false,
1715 message: match &monitor {
1716 Some(name) => Some(format!("Unknown monitor: {name}")),
1717 None => Some("No outputs available".into()),
1718 },
1719 };
1720 }
1721 let mut blanked_count = 0u32;
1722 for (name, rs) in render_states.iter() {
1723 if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
1724 continue;
1725 }
1726 let mut lock = rs.lock().await;
1727 if lock.blanked {
1728 continue;
1729 }
1730 lock.pre_blank = Some((
1732 lock.last_wallpaper.clone().unwrap_or_default(),
1733 lock.scaling_mode,
1734 ));
1735 lock.blanked = true;
1736 let black_effect = crate::animation::Effect::Fade(
1738 crate::animation::FadeParams::default(),
1739 );
1740 let tmp = std::env::temp_dir().join("wallr_blank.png");
1742 {
1743 let img =
1744 image::RgbaImage::from_pixel(1, 1, image::Rgba([0, 0, 0, 255]));
1745 let _ = img.save(&tmp);
1746 }
1747 let _ = lock.set_wallpaper(&tmp, &black_effect, 200, 0).await;
1748 blanked_count += 1;
1749 }
1750 IpcResponse {
1751 success: true,
1752 message: Some(format!("Blanked {blanked_count} output(s)")),
1753 }
1754 }
1755 IpcCommand::Restore { monitor } => {
1756 let targets = resolve_targets(&render_states, monitor.as_deref()).await;
1757 if targets.is_empty() {
1758 return IpcResponse {
1759 success: false,
1760 message: match &monitor {
1761 Some(name) => Some(format!("Unknown monitor: {name}")),
1762 None => Some("No outputs available".into()),
1763 },
1764 };
1765 }
1766 let mut restored_count = 0u32;
1767 let mut errors = Vec::new();
1768 for (name, rs) in render_states.iter() {
1769 if monitor.as_deref() != Some(name.as_str()) && monitor.is_some() {
1770 continue;
1771 }
1772 let mut lock = rs.lock().await;
1773 if !lock.blanked {
1774 continue;
1775 }
1776 if let Some((ref path, scaling_mode)) = lock.pre_blank.clone() {
1777 if !path.exists() {
1779 errors.push(format!("{}: wallpaper path no longer exists", name));
1780 lock.blanked = false;
1781 lock.pre_blank = None;
1782 continue;
1783 }
1784 let restore_effect = crate::animation::Effect::Fade(
1785 crate::animation::FadeParams::default(),
1786 );
1787 match lock
1788 .set_wallpaper(
1789 std::path::Path::new(&path),
1790 &restore_effect,
1791 200,
1792 scaling_mode,
1793 )
1794 .await
1795 {
1796 Ok(_) => {
1797 lock.blanked = false;
1798 lock.pre_blank = None;
1799 restored_count += 1;
1800 }
1801 Err(e) => {
1802 errors.push(format!("{}: restore failed: {}", name, e));
1803 lock.blanked = false;
1804 lock.pre_blank = None;
1805 }
1806 }
1807 } else {
1808 errors.push(format!("{}: no previous wallpaper to restore", name));
1809 lock.blanked = false;
1810 }
1811 }
1812 if !errors.is_empty() {
1813 IpcResponse {
1814 success: restored_count > 0,
1815 message: Some(format!(
1816 "Restored {} output(s), {} error(s): {}",
1817 restored_count,
1818 errors.len(),
1819 errors.join("; ")
1820 )),
1821 }
1822 } else {
1823 IpcResponse {
1824 success: true,
1825 message: Some(format!("Restored {restored_count} output(s)")),
1826 }
1827 }
1828 }
1829 }
1830 }
1831 })
1832 .await?;
1833
1834 if self.config.watch.enabled
1836 && let Some(ref watch_dir) = self.config.watch.dir
1837 {
1838 let watch_path = crate::config::expand_path(watch_dir);
1839 self.start_watcher(watch_path, render_states.clone())
1840 .await?;
1841 }
1842
1843 tokio::task::spawn_blocking(move || {
1844 loop {
1845 if let Err(e) = event_queue.blocking_dispatch(&mut wayland_state) {
1846 eprintln!("Wayland dispatch error: {e:?}");
1847 break;
1848 }
1849 }
1850 eprintln!("wallr: Wayland connection lost, exiting");
1854 std::process::exit(1);
1855 });
1856
1857 loop {
1858 tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
1859 }
1860 }
1861
1862 async fn start_watcher(
1863 &self,
1864 dir: PathBuf,
1865 render_states: std::sync::Arc<
1866 tokio::sync::Mutex<std::collections::HashMap<String, Arc<Mutex<RenderState>>>>,
1867 >,
1868 ) -> Result<(), DaemonError> {
1869 let engine = self.engine.clone();
1870 let paused = self.paused.clone();
1871 let debounce = crate::config::parse_duration(&self.config.watch.debounce)
1872 .unwrap_or(std::time::Duration::from_millis(500));
1873
1874 let (tx, mut rx) = tokio::sync::mpsc::channel(100);
1875
1876 let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
1877 if let Ok(event) = res
1878 && let EventKind::Create(_) = event.kind
1879 {
1880 for path in event.paths {
1881 let _ = tx.blocking_send(path);
1882 }
1883 }
1884 })
1885 .map_err(|e| DaemonError::StartError(e.to_string()))?;
1886
1887 watcher
1888 .watch(&dir, RecursiveMode::NonRecursive)
1889 .map_err(|e| DaemonError::StartError(e.to_string()))?;
1890
1891 tokio::spawn(async move {
1892 let _watcher = watcher;
1893 let mut last: Option<(PathBuf, std::time::Instant)> = None;
1894
1895 while let Some(path) = rx.recv().await {
1896 if paused.load(Ordering::SeqCst) {
1897 continue;
1898 }
1899 if let Some((ref lp, ref lt)) = last
1900 && lp == &path
1901 && lt.elapsed() < debounce
1902 {
1903 continue;
1904 }
1905 let ext = path
1906 .extension()
1907 .unwrap_or_default()
1908 .to_string_lossy()
1909 .to_lowercase();
1910 if !["jpg", "jpeg", "png", "gif", "webp"].contains(&ext.as_str()) {
1911 continue;
1912 }
1913 last = Some((path.clone(), std::time::Instant::now()));
1914
1915 let states = render_states.lock().await;
1917 for (name, rs) in states.iter() {
1918 let rs = rs.clone();
1919 let eng = engine.clone();
1920 let p = path.clone();
1921 let name = name.clone();
1922 tokio::spawn(async move {
1923 let mut lock = rs.lock().await;
1924 let effect =
1925 crate::animation::Effect::Fade(crate::animation::FadeParams::default());
1926 let _ = lock.set_wallpaper(&p, &effect, 600, 0).await;
1927 drop(lock);
1928 let opts = SetOptions {
1929 no_theme: false,
1930 theme_provider: None,
1931 monitor: Some(name),
1932 };
1933 let mut elock = eng.lock().await;
1934 let _ = elock.set_wallpaper(&p, &opts).await;
1935 });
1936 }
1937 }
1938 });
1939
1940 Ok(())
1941 }
1942
1943 #[allow(clippy::too_many_arguments)]
1946 async fn create_render_state_for_output(
1947 renderer: &std::sync::Arc<Renderer>,
1948 display_ptr: *mut std::ffi::c_void,
1949 wayland_state: &mut WaylandState,
1950 qh: &QueueHandle<WaylandState>,
1951 output: &OutputInfo,
1952 config: &WallrConfig,
1953 ) -> Result<RenderState, DaemonError> {
1954 let wl_surface = wayland_state.compositor_state.create_surface(qh);
1955 let layer_surface = wayland_state.layer_shell.create_layer_surface(
1956 qh,
1957 wl_surface,
1958 Layer::Background,
1959 Some("wallr"),
1960 Some(&output.wl_output),
1961 );
1962 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
1963 layer_surface.set_exclusive_zone(-1);
1964 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
1965
1966 let empty_region = wayland_state.compositor.create_region(qh, ());
1968 layer_surface
1969 .wl_surface()
1970 .set_input_region(Some(&empty_region));
1971 layer_surface.commit();
1972 empty_region.destroy();
1973
1974 let scale_factor = if output.scale_factor > 0 {
1975 output.scale_factor
1976 } else {
1977 1
1978 };
1979 layer_surface.wl_surface().set_buffer_scale(scale_factor);
1980
1981 let width = output.width * scale_factor as u32;
1982 let height = output.height * scale_factor as u32;
1983
1984 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
1985 wayland_state
1986 .surfaces
1987 .push((output.wl_output.id().protocol_id(), layer_surface));
1988
1989 let window_handle = WaylandWindow {
1990 display: display_ptr,
1991 surface: raw_surface,
1992 };
1993
1994 let wgpu_surface = renderer
1995 .instance
1996 .create_surface(&window_handle)
1997 .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
1998
1999 let adapter = renderer
2000 .instance
2001 .request_adapter(&wgpu::RequestAdapterOptions {
2002 compatible_surface: Some(&wgpu_surface),
2003 power_preference: wgpu::PowerPreference::HighPerformance,
2004 force_fallback_adapter: false,
2005 })
2006 .await;
2007 let surf_format = adapter
2008 .as_ref()
2009 .map(|a| {
2010 let caps = wgpu_surface.get_capabilities(a);
2011 caps.formats
2012 .into_iter()
2013 .next()
2014 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
2015 })
2016 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb);
2017
2018 let surf_config = wgpu::SurfaceConfiguration {
2019 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2020 format: surf_format,
2021 width,
2022 height,
2023 present_mode: wgpu::PresentMode::Fifo,
2024 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
2025 view_formats: vec![],
2026 desired_maximum_frame_latency: 2,
2027 };
2028 wgpu_surface.configure(&renderer.device, &surf_config);
2029
2030 let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
2033 let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
2034
2035 Ok(RenderState {
2036 renderer: renderer.clone(),
2037 surface,
2038 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
2039 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
2040 pacer: std::sync::Arc::new(LivePacer::new()),
2041 current_bind: None,
2042 current_tex: None,
2043 width,
2044 height,
2045 current_width: 0,
2046 current_height: 0,
2047 format: surf_format,
2048 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
2049 hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
2050 scaling_mode: 0,
2051 per_output_uniforms: std::sync::Arc::new(renderer.create_per_output_uniforms()),
2052 last_wallpaper: None,
2053 pre_blank: None,
2054 blanked: false,
2055 gif_paused: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2056 })
2057 }
2058}
2059
2060#[allow(clippy::too_many_arguments)]
2064fn create_render_state_for_output_sync(
2065 renderer: &std::sync::Arc<Renderer>,
2066 display_ptr: *mut std::ffi::c_void,
2067 wayland_state: &mut WaylandState,
2068 qh: &QueueHandle<WaylandState>,
2069 output: &OutputInfo,
2070 config: &WallrConfig,
2071) -> Result<RenderState, DaemonError> {
2072 let wl_surface = wayland_state.compositor_state.create_surface(qh);
2073 let layer_surface = wayland_state.layer_shell.create_layer_surface(
2074 qh,
2075 wl_surface,
2076 Layer::Background,
2077 Some("wallr"),
2078 Some(&output.wl_output),
2079 );
2080 layer_surface.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
2081 layer_surface.set_exclusive_zone(-1);
2082 layer_surface.set_keyboard_interactivity(KeyboardInteractivity::None);
2083
2084 let empty_region = wayland_state.compositor.create_region(qh, ());
2085 layer_surface
2086 .wl_surface()
2087 .set_input_region(Some(&empty_region));
2088 layer_surface.commit();
2089 empty_region.destroy();
2090
2091 let scale_factor = if output.scale_factor > 0 {
2092 output.scale_factor
2093 } else {
2094 1
2095 };
2096 layer_surface.wl_surface().set_buffer_scale(scale_factor);
2097
2098 let width = output.width * scale_factor as u32;
2099 let height = output.height * scale_factor as u32;
2100
2101 let raw_surface = layer_surface.wl_surface().id().as_ptr() as *mut std::ffi::c_void;
2102 wayland_state
2103 .surfaces
2104 .push((output.wl_output.id().protocol_id(), layer_surface));
2105
2106 let window_handle = WaylandWindow {
2107 display: display_ptr,
2108 surface: raw_surface,
2109 };
2110
2111 let wgpu_surface = renderer
2112 .instance
2113 .create_surface(&window_handle)
2114 .map_err(|e| DaemonError::StartError(format!("wgpu surface creation failed: {e:?}")))?;
2115
2116 let surf_format = {
2117 let caps = wgpu_surface.get_capabilities(&renderer.adapter);
2118 caps.formats
2119 .into_iter()
2120 .next()
2121 .unwrap_or(wgpu::TextureFormat::Bgra8UnormSrgb)
2122 };
2123
2124 let surf_config = wgpu::SurfaceConfiguration {
2125 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
2126 format: surf_format,
2127 width,
2128 height,
2129 present_mode: wgpu::PresentMode::Fifo,
2130 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
2131 view_formats: vec![],
2132 desired_maximum_frame_latency: 2,
2133 };
2134 wgpu_surface.configure(&renderer.device, &surf_config);
2135
2136 let wgpu_surface: wgpu::Surface<'static> = unsafe { std::mem::transmute(wgpu_surface) };
2137 let surface: &'static wgpu::Surface<'static> = Box::leak(Box::new(wgpu_surface));
2138
2139 Ok(RenderState {
2140 renderer: renderer.clone(),
2141 surface,
2142 render_lock: std::sync::Arc::new(std::sync::Mutex::new(())),
2143 playback_gen: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
2144 pacer: std::sync::Arc::new(LivePacer::new()),
2145 current_bind: None,
2146 current_tex: None,
2147 width,
2148 height,
2149 current_width: 0,
2150 current_height: 0,
2151 format: surf_format,
2152 video_playback: std::sync::Arc::new(crate::video::VideoPlayback::new()),
2153 hw_accel: crate::video::HwAccel::from_config(&config.video.hw_decode),
2154 scaling_mode: 0,
2155 per_output_uniforms: std::sync::Arc::new(renderer.create_per_output_uniforms()),
2156 last_wallpaper: None,
2157 pre_blank: None,
2158 blanked: false,
2159 gif_paused: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
2160 })
2161}