mod wayland; use anyhow::Result; use clap::{Arg, Command}; use log::{error, info, warn}; use std::mem; use std::time::Instant; use wgpu::util::DeviceExt; use winit::{ application::ApplicationHandler, event::WindowEvent, event_loop::{ActiveEventLoop, EventLoop}, window::{Window, WindowAttributes, WindowId}, }; #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] struct Uniforms { i_resolution: [f32; 3], i_time: f32, i_mouse: [f32; 4], } struct WallpaperApp { window: Option, surface: Option>, device: Option, queue: Option, config: Option, render_pipeline: Option, uniform_buffer: Option, bind_group: Option, start_time: Instant, shader_path: String, hyprland_config: wayland::HyprlandConfig, wayland_configured: bool, } impl WallpaperApp { fn new(shader_path: String) -> Self { Self { window: None, surface: None, device: None, queue: None, config: None, render_pipeline: None, uniform_buffer: None, bind_group: None, start_time: Instant::now(), shader_path, hyprland_config: wayland::HyprlandConfig::default(), wayland_configured: false, } } fn init_graphics(&mut self) -> Result<()> { let window = self .window .as_ref() .ok_or_else(|| anyhow::anyhow!("Window not initialized"))?; let size = window.inner_size(); let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { memory_budget_thresholds: wgpu::MemoryBudgetThresholds { for_resource_creation: Some(200), for_device_loss: Some(100), }, backends: wgpu::Backends::VULKAN, backend_options: wgpu::BackendOptions { gl: wgpu::GlBackendOptions::default(), dx12: wgpu::Dx12BackendOptions::default(), noop: wgpu::NoopBackendOptions::default(), }, flags: wgpu::InstanceFlags::empty(), }); // Create surface let surface = instance .create_surface(window) .map_err(|e| anyhow::anyhow!("Failed to create surface: {}", e))?; let surface = unsafe { mem::transmute::, wgpu::Surface<'static>>(surface) }; let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, compatible_surface: Some(&surface), force_fallback_adapter: false, })) .unwrap(); let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor { required_features: wgpu::Features::empty(), required_limits: wgpu::Limits::default(), memory_hints: wgpu::MemoryHints::Performance, label: None, experimental_features: wgpu::ExperimentalFeatures::disabled(), trace: wgpu::Trace::Off, }))?; let surface_caps = surface.get_capabilities(&adapter); let surface_format = surface_caps .formats .iter() .copied() .find(|f| f.is_srgb()) .unwrap_or(surface_caps.formats[0]); let config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: surface_format, width: size.width, height: size.height, present_mode: surface_caps.present_modes[0], alpha_mode: surface_caps.alpha_modes[0], view_formats: vec![], desired_maximum_frame_latency: 2, }; surface.configure(&device, &config); // Load and compile shader let shader_source = std::fs::read_to_string(&self.shader_path).map_err(|e| { anyhow::anyhow!("Failed to read shader file {}: {}", &self.shader_path, e) })?; let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("Wallpaper Shader"), source: wgpu::ShaderSource::Wgsl(shader_source.as_str().into()), }); // Create uniform buffer let uniforms = Uniforms { i_resolution: [size.width as f32, size.height as f32, 0.0], i_time: 0.0, i_mouse: [0.0, 0.0, 0.0, 0.0], }; let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Uniform Buffer"), contents: bytemuck::cast_slice(&[uniforms]), usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, }); let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { entries: &[wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }], label: Some("uniform_bind_group_layout"), }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform_buffer.as_entire_binding(), }], label: Some("uniform_bind_group"), }); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Render Pipeline Layout"), bind_group_layouts: &[&bind_group_layout], push_constant_ranges: &[], }); let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Render Pipeline"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &shader_module, entry_point: Some("vs_main"), compilation_options: wgpu::PipelineCompilationOptions::default(), buffers: &[], }, fragment: Some(wgpu::FragmentState { module: &shader_module, entry_point: Some("fs_main"), compilation_options: wgpu::PipelineCompilationOptions::default(), targets: &[Some(wgpu::ColorTargetState { format: config.format, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, strip_index_format: None, front_face: wgpu::FrontFace::Ccw, cull_mode: Some(wgpu::Face::Back), polygon_mode: wgpu::PolygonMode::Fill, unclipped_depth: false, conservative: false, }, depth_stencil: None, multisample: wgpu::MultisampleState { count: 1, mask: !0, alpha_to_coverage_enabled: false, }, multiview: None, cache: None, }); self.surface = Some(surface); self.device = Some(device); self.queue = Some(queue); self.config = Some(config); self.render_pipeline = Some(render_pipeline); self.uniform_buffer = Some(uniform_buffer); self.bind_group = Some(bind_group); Ok(()) } fn resize(&mut self, new_size: winit::dpi::PhysicalSize) { if new_size.width > 0 && new_size.height > 0 && let (Some(surface), Some(device), Some(config)) = (&self.surface, &self.device, &mut self.config) { config.width = new_size.width; config.height = new_size.height; surface.configure(device, config); } } fn render(&mut self) -> Result<()> { let surface = self .surface .as_ref() .ok_or_else(|| anyhow::anyhow!("Surface not initialized"))?; let device = self .device .as_ref() .ok_or_else(|| anyhow::anyhow!("Device not initialized"))?; let queue = self .queue .as_ref() .ok_or_else(|| anyhow::anyhow!("Queue not initialized"))?; let render_pipeline = self .render_pipeline .as_ref() .ok_or_else(|| anyhow::anyhow!("Render pipeline not initialized"))?; let uniform_buffer = self .uniform_buffer .as_ref() .ok_or_else(|| anyhow::anyhow!("Uniform buffer not initialized"))?; let bind_group = self .bind_group .as_ref() .ok_or_else(|| anyhow::anyhow!("Bind group not initialized"))?; let config = self .config .as_ref() .ok_or_else(|| anyhow::anyhow!("Config not initialized"))?; let frame = surface.get_current_texture()?; let view = frame .texture .create_view(&wgpu::TextureViewDescriptor::default()); // Update uniforms let uniforms = Uniforms { i_resolution: [config.width as f32, config.height as f32, 0.0], i_time: self.start_time.elapsed().as_secs_f32(), i_mouse: [0.0, 0.0, 0.0, 0.0], }; queue.write_buffer(uniform_buffer, 0, bytemuck::cast_slice(&[uniforms])); let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder"), }); { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Render Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { depth_slice: None, view: &view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, occlusion_query_set: None, timestamp_writes: None, }); render_pass.set_pipeline(render_pipeline); render_pass.set_bind_group(0, bind_group, &[]); render_pass.draw(0..3, 0..1); } queue.submit(std::iter::once(encoder.finish())); frame.present(); Ok(()) } } impl ApplicationHandler for WallpaperApp { fn resumed(&mut self, event_loop: &ActiveEventLoop) { if self.window.is_none() { let window_attributes = WindowAttributes::default() .with_title("Hyprland Live Wallpaper") .with_transparent(true) .with_decorations(false) .with_fullscreen(Some(winit::window::Fullscreen::Borderless(None))); let window = match event_loop.create_window(window_attributes) { Ok(window) => window, Err(e) => { error!("Failed to create window: {}", e); return; } }; // Configure window for Hyprland if let Err(e) = wayland::configure_hyprland_window(&window, &self.hyprland_config) { warn!("Failed to configure Hyprland window: {}", e); } // Store window first, then initialize graphics self.window = Some(window); match self.init_graphics() { Ok(()) => { info!("Wallpaper service running"); } Err(e) => { error!("Failed to initialize graphics: {}", e); } } } } fn window_event( &mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent, ) { match event { WindowEvent::CloseRequested => { event_loop.exit(); } WindowEvent::Resized(size) => { self.resize(size); } WindowEvent::RedrawRequested => { // Apply Hyprland rules on first redraw (after window is mapped) if !self.wayland_configured && let Some(window) = &self.window { // Convert window ID to u64 for hyprctl let window_id: u64 = window.id().into(); if let Err(e) = wayland::apply_hyprland_rules(window_id, &self.hyprland_config) { warn!("Failed to apply Hyprland rules: {}", e); } self.wayland_configured = true; } if let Err(e) = self.render() { error!("Render error: {}", e); event_loop.exit(); } } _ => {} } } fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { if let Some(window) = &self.window { window.request_redraw(); } } } fn main() -> Result<()> { env_logger::init(); let matches = Command::new("hyprland-live-wallpaper") .version("0.1.0") .about("Live wallpaper service for Hyprland using wgpu") .arg( Arg::new("shader") .short('s') .long("shader") .value_name("FILE") .help("Path to WGSL shader file"), ) .get_matches(); info!("Starting Hyprland Live Wallpaper"); let shader_path = matches .get_one::("shader") .map(|s| s.as_str()) .unwrap_or("shaders/red.wgsl") .to_string(); run_wallpaper(shader_path)?; Ok(()) } fn run_wallpaper(shader_path: String) -> Result<()> { let event_loop = EventLoop::new()?; let mut app = WallpaperApp::new(shader_path); event_loop.run_app(&mut app)?; Ok(()) }