diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 413 |
1 files changed, 413 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d58cae1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,413 @@ +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<Window>, + surface: Option<wgpu::Surface<'static>>, + device: Option<wgpu::Device>, + queue: Option<wgpu::Queue>, + config: Option<wgpu::SurfaceConfiguration>, + render_pipeline: Option<wgpu::RenderPipeline>, + uniform_buffer: Option<wgpu::Buffer>, + bind_group: Option<wgpu::BindGroup>, + 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::default(); + + // 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<'_>, 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::downlevel_defaults(), + 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<u32>) { + if new_size.width > 0 && new_size.height > 0 { + if 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 { + if 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::<String>("shader") + .map(|s| s.as_str()) + .unwrap_or("shaders/default.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(()) +} |
