aboutsummaryrefslogtreecommitdiffstats
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs413
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(())
+}