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