commit f53340ad8e584923f71754a4da46bb36f87952f2 Author: Bendik Aagaard Lynghaug Date: Wed Apr 29 21:19:55 2026 +0200 initial rewrite of glpaper using rust wgpu diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad51faa --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target +Cargo.lock +shaders diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0d64420 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "gpupaper" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "gpupaper" +path = "src/main.rs" + +[dependencies] +smithay-client-toolkit = "0.20" +wgpu = { version = "29", features = ["wgsl", "glsl"] } +raw-window-handle = "0.6" +anyhow = "1" +clap = { version = "4", features = ["derive"] } +bytemuck = { version = "1", features = ["derive"] } +log = "0.4" +env_logger = "0.11" +wayland-client = { version = "0.31", features = ["system"] } +wayland-backend = { version = "0.3", features = ["client_system"] } +naga = "29" +pollster = "0.4" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..94c69bb --- /dev/null +++ b/src/main.rs @@ -0,0 +1,40 @@ +//! gpupaper – Wayland background shader runner. + +mod renderer; +mod shader; +mod wayland; + +use std::path::PathBuf; + +use anyhow::Result; +use clap::Parser; + +/// Run a GLSL/WGSL fragment shader as a fullscreen Wayland layer surface. +#[derive(Parser, Debug)] +#[command(author, version, about)] +struct Args { + /// Wayland output name (e.g. "HDMI-A-1") or "all" / "*" for every output. + output: String, + + /// Path to the fragment shader file (.frag / .glsl = GLSL, .wgsl = WGSL). + shader_file: PathBuf, + + /// Target frame rate. 0 = vsync (default). + #[arg(long, default_value = "0")] + fps: u32, + + /// Layer to render on: background, bottom, top, overlay. + #[arg(long, default_value = "background")] + layer: String, +} + +fn main() -> Result<()> { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + let args = Args::parse(); + + let layer_kind = wayland::parse_layer(&args.layer)?; + let shader = shader::load(&args.shader_file)?; + + wayland::run(args.output, shader, args.fps, layer_kind) +} diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..418c393 --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,372 @@ +//! wgpu device, surface, pipeline setup and render loop. + +use std::time::Instant; +use anyhow::{Context, Result}; +use bytemuck::{Pod, Zeroable}; +use raw_window_handle::{ + HasDisplayHandle, RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, +}; + +#[derive(Debug)] +struct DisplayHandleWrapper(RawDisplayHandle); +impl HasDisplayHandle for DisplayHandleWrapper { + fn display_handle( + &self, + ) -> Result, raw_window_handle::HandleError> { + unsafe { Ok(raw_window_handle::DisplayHandle::borrow_raw(self.0)) } + } +} +unsafe impl Send for DisplayHandleWrapper {} +unsafe impl Sync for DisplayHandleWrapper {} +use std::ptr::NonNull; +use wgpu::util::DeviceExt; + +use crate::shader::ShaderSource; + +/// Uniform data sent to the fragment shader each frame. +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +pub struct Uniforms { + pub time: f32, + pub _pad: f32, + pub resolution: [f32; 2], + pub mouse: [f32; 2], +} + +/// The full wgpu rendering context for one layer surface. +pub struct Renderer { + device: wgpu::Device, + queue: wgpu::Queue, + surface: wgpu::Surface<'static>, + surface_config: wgpu::SurfaceConfiguration, + pipeline: wgpu::RenderPipeline, + uniform_buf: wgpu::Buffer, + bind_group: wgpu::BindGroup, + start: Instant, + width: u32, + height: u32, +} + +/// Vertex shader (WGSL) – used when the fragment shader is also WGSL. +const VERTEX_SHADER_WGSL: &str = r#" +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, +}; + +@vertex +fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput { + var pos = array, 3>( + vec2(-1.0, -1.0), + vec2( 3.0, -1.0), + vec2(-1.0, 3.0), + ); + var uv = array, 3>( + vec2(0.0, 0.0), + vec2(2.0, 0.0), + vec2(0.0, 2.0), + ); + var out: VertexOutput; + out.position = vec4(pos[vi], 0.0, 1.0); + out.uv = uv[vi]; + return out; +} +"#; + +/// Vertex shader (GLSL) – used when the fragment shader is GLSL so that +/// naga sees consistent interpolation qualifiers across both stages. +const VERTEX_SHADER_GLSL: &str = r#"#version 450 + +layout(location = 0) out vec2 v_uv; + +void main() { + vec2 pos[3] = vec2[3]( + vec2(-1.0, -1.0), + vec2( 3.0, -1.0), + vec2(-1.0, 3.0) + ); + vec2 uv[3] = vec2[3]( + vec2(0.0, 0.0), + vec2(2.0, 0.0), + vec2(0.0, 2.0) + ); + gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0); + v_uv = uv[gl_VertexIndex]; +} +"#; + +impl Renderer { + /// Create a new renderer from raw Wayland display/surface pointers. + /// + /// # Safety + /// The caller must guarantee that `display_ptr` and `surface_ptr` are + /// valid Wayland pointers that outlive this `Renderer`. + pub unsafe fn new( + display_ptr: *mut std::ffi::c_void, + surface_ptr: *mut std::ffi::c_void, + width: u32, + height: u32, + shader: &ShaderSource, + fps: u32, + ) -> Result { + let raw_display = RawDisplayHandle::Wayland(WaylandDisplayHandle::new( + NonNull::new(display_ptr).context("null display pointer")?, + )); + let raw_window = RawWindowHandle::Wayland(WaylandWindowHandle::new( + NonNull::new(surface_ptr).context("null surface pointer")?, + )); + + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::GL, + flags: wgpu::InstanceFlags::default(), + memory_budget_thresholds: Default::default(), + backend_options: Default::default(), + display: Some(Box::new(DisplayHandleWrapper(raw_display))), + }); + + let wgpu_surface = instance + .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle { + raw_display_handle: Some(raw_display), + raw_window_handle: raw_window, + }) + .context("failed to create wgpu surface")?; + + let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface: Some(&wgpu_surface), + force_fallback_adapter: false, + })) + .context("no suitable wgpu adapter found")?; + + log::info!("wgpu adapter: {:?}", adapter.get_info()); + + let (device, queue) = pollster::block_on(adapter.request_device( + &wgpu::DeviceDescriptor { + label: Some("gpupaper"), + required_features: wgpu::Features::empty(), + required_limits: adapter.limits(), + memory_hints: wgpu::MemoryHints::default(), + experimental_features: Default::default(), + trace: Default::default(), + }, + )) + .context("failed to create wgpu device")?; + + // Surface configuration + let caps = wgpu_surface.get_capabilities(&adapter); + let format = caps + .formats + .iter() + .copied() + .find(|f| !f.is_srgb()) + .unwrap_or(caps.formats[0]); + + let present_mode = if fps == 0 || !caps.present_modes.contains(&wgpu::PresentMode::Immediate) { + wgpu::PresentMode::Fifo + } else { + wgpu::PresentMode::Immediate + }; + + let surface_config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format, + view_formats: vec![], + alpha_mode: wgpu::CompositeAlphaMode::Opaque, + width, + height, + desired_maximum_frame_latency: 2, + present_mode, + }; + wgpu_surface.configure(&device, &surface_config); + + // Uniform buffer + let initial_uniforms = Uniforms { + time: 0.0, + _pad: 0.0, + resolution: [width as f32, height as f32], + mouse: [0.0, 0.0], + }; + let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("uniforms"), + contents: bytemuck::bytes_of(&initial_uniforms), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("uniforms layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("uniforms bind group"), + layout: &bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buf.as_entire_binding(), + }], + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("pipeline layout"), + bind_group_layouts: &[Some(&bind_group_layout)], + immediate_size: 0, + }); + + let (vs_module, fs_module) = match shader { + ShaderSource::Wgsl(src) => ( + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("vertex shader (wgsl)"), + source: wgpu::ShaderSource::Wgsl(VERTEX_SHADER_WGSL.into()), + }), + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("fragment shader (wgsl)"), + source: wgpu::ShaderSource::Wgsl(src.as_str().into()), + }), + ), + ShaderSource::Glsl(src) => ( + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("vertex shader (glsl)"), + source: wgpu::ShaderSource::Glsl { + shader: VERTEX_SHADER_GLSL.into(), + stage: naga::ShaderStage::Vertex, + defines: &[], + }, + }), + device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("fragment shader (glsl)"), + source: wgpu::ShaderSource::Glsl { + shader: src.as_str().into(), + stage: naga::ShaderStage::Fragment, + defines: &[], + }, + }), + ), + }; + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("render pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &vs_module, + entry_point: None, + compilation_options: Default::default(), + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &fs_module, + entry_point: Some("main"), + compilation_options: Default::default(), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + + Ok(Self { + device, + queue, + surface: wgpu_surface, + surface_config, + pipeline, + uniform_buf, + bind_group, + start: Instant::now(), + width, + height, + }) + } + + /// Resize the surface (called when the layer surface configure changes). + pub fn resize(&mut self, width: u32, height: u32) { + if width == 0 || height == 0 { + return; + } + self.width = width; + self.height = height; + self.surface_config.width = width; + self.surface_config.height = height; + self.surface.configure(&self.device, &self.surface_config); + log::debug!("resized to {}x{}", width, height); + } + + /// Render one frame. Returns `false` if the surface was lost/outdated. + pub fn render(&mut self) -> bool { + let elapsed = self.start.elapsed().as_secs_f32(); + let uniforms = Uniforms { + time: elapsed, + _pad: 0.0, + resolution: [self.width as f32, self.height as f32], + mouse: [0.0, 0.0], + }; + self.queue.write_buffer(&self.uniform_buf, 0, bytemuck::bytes_of(&uniforms)); + + let surface_texture = match self.surface.get_current_texture() { + wgpu::CurrentSurfaceTexture::Success(t) => t, + wgpu::CurrentSurfaceTexture::Suboptimal(t) => { + // Reconfigure on next frame but still render this one + self.surface.configure(&self.device, &self.surface_config); + t + } + wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Outdated => { + self.surface.configure(&self.device, &self.surface_config); + return false; + } + wgpu::CurrentSurfaceTexture::Timeout + | wgpu::CurrentSurfaceTexture::Occluded + | wgpu::CurrentSurfaceTexture::Validation => { + return false; + } + }; + + let view = + surface_texture.texture.create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("frame encoder"), + }); + + { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("main pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + multiview_mask: None, + }); + rpass.set_pipeline(&self.pipeline); + rpass.set_bind_group(0, &self.bind_group, &[]); + rpass.draw(0..3, 0..1); + } + + self.queue.submit(Some(encoder.finish())); + surface_texture.present(); + true + } +} diff --git a/src/shader.rs b/src/shader.rs new file mode 100644 index 0000000..92f4134 --- /dev/null +++ b/src/shader.rs @@ -0,0 +1,319 @@ +//! Shader loading and GLSL-wrapping logic. + +use std::path::Path; +use anyhow::{Context, Result}; + +/// The result of loading a shader file. +pub enum ShaderSource { + /// A WGSL shader, ready to pass to wgpu as-is. + Wgsl(String), + /// A GLSL fragment shader, fully wrapped in the UBO template. + Glsl(String), +} + +/// Load a shader from disk. `.wgsl` files are returned verbatim. +/// `.frag` / `.glsl` files are preprocessed and wrapped to work with +/// wgpu's GLSL frontend. Two source styles are supported: +/// +/// - **mainImage** (ShaderToy / glpaper hybrid): has `mainImage(out vec4, in vec2)`. +/// Transformed so naga's GLSL frontend (which mis-parses `out` function params) +/// sees it as `vec4 mainImage(vec2)` instead. +/// - **glpaper native**: has `void main()` and writes `gl_FragColor` directly. +/// `gl_FragColor` is rewritten to `out_color`. +pub fn load(path: &Path) -> Result { + let source = std::fs::read_to_string(path) + .with_context(|| format!("failed to read shader file: {}", path.display()))?; + + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + if ext == "wgsl" { + return Ok(ShaderSource::Wgsl(source)); + } + + let mut user_code = source.clone(); + + // Remove #ifdef GL_ES / precision / #endif compatibility blocks. + user_code = strip_gles_compat(&user_code); + + // Remove bare uniform declarations for the variables we supply via UBO. + for (ty, var) in &[("float", "time"), ("vec2", "resolution"), ("vec2", "mouse")] { + user_code = strip_uniform(&user_code, ty, var); + } + + let preamble = r#"#version 450 + +layout(location = 0) in vec2 v_uv; +layout(location = 0) out vec4 out_color; + +layout(set = 0, binding = 0) uniform Uniforms { + float time; + float _pad; + vec2 resolution; + vec2 mouse; +}; + +"#; + + let wrapped = if source.contains("mainImage") { + // Naga's GLSL frontend mis-parses `out vec4` in non-entry-point function + // parameters, causing "Expected end of file, found LeftBrace". + // Transform: void mainImage(out vec4 NAME, in vec2 COORD) { … } + // → vec4 mainImage(vec2 COORD) { vec4 NAME; … return NAME; } + user_code = transform_main_image(&user_code); + + // Strip forward declarations: naga may mishandle `inout` prototypes. + // Naga does two-pass function collection so prototypes aren't needed. + user_code = strip_prototypes(&user_code); + + // Strip any existing void main() — we provide a controlled wrapper. + user_code = strip_void_main(&user_code); + + format!( + "{preamble} +// --- user shader --- +{user_code} +// --- end user shader --- + +void main() {{ + vec2 fc = vec2(gl_FragCoord.x, resolution.y - gl_FragCoord.y); + out_color = mainImage(fc); +}} +" + ) + } else { + // Native glpaper style: shader owns void main() and writes gl_FragColor. + user_code = user_code.replace("gl_FragColor", "out_color"); + // wgpu's GL backend presents with Y flipped; correct by flipping gl_FragCoord.y. + user_code = user_code.replace( + "gl_FragCoord.xy", + "vec2(gl_FragCoord.x, resolution.y - gl_FragCoord.y)", + ); + format!( + "{preamble} +// --- user shader --- +{user_code} +// --- end user shader --- +" + ) + }; + + Ok(ShaderSource::Glsl(wrapped)) +} + +/// Transform `void mainImage(out vec4 NAME, in vec2 COORD) { … }` into +/// `vec4 mainImage(vec2 COORD) { vec4 NAME; … return NAME; }`. +/// +/// naga's GLSL 450 frontend mis-parses `out vec4` in ordinary (non-entry-point) +/// function parameters, leaving a `{` unexpectedly at the top level. +/// Using a return value instead sidesteps the issue entirely. +fn transform_main_image(src: &str) -> String { + let lines: Vec<&str> = src.lines().collect(); + let mut result: Vec = Vec::with_capacity(lines.len() + 4); + let mut i = 0; + + while i < lines.len() { + let t = lines[i].trim(); + + // Detect a mainImage *definition* (not a prototype): has "out vec4" and no trailing `;` + if t.contains("mainImage") && t.contains("out vec4") && !t.ends_with(';') { + // Collect lines until we find the one containing the opening `{`. + let mut sig_parts: Vec<&str> = Vec::new(); + let mut brace_line: Option = None; + + let mut k = i; + while k < lines.len() { + sig_parts.push(lines[k]); + if lines[k].contains('{') { + brace_line = Some(k); + break; + } + k += 1; + if k - i > 10 { break; } // safety + } + + let full_sig = sig_parts.iter().map(|s| s.trim()).collect::>().join(" "); + + if let Some((out_name, coord_name)) = extract_mainimage_params(&full_sig) { + // Emit new signature + body opener + result.push(format!("vec4 mainImage(vec2 {}) {{", coord_name)); + result.push(format!(" vec4 {};", out_name)); + + // Advance past the signature lines and the `{` line + i = brace_line.map_or(k, |b| b) + 1; + + // Collect function body with brace counting; inject return before final `}` + let mut depth: i32 = 1; + let mut body: Vec<&str> = Vec::new(); + + while i < lines.len() { + let fl = lines[i]; + let opens = fl.chars().filter(|&c| c == '{').count() as i32; + let closes = fl.chars().filter(|&c| c == '}').count() as i32; + depth += opens - closes; + body.push(fl); + i += 1; + if depth <= 0 { break; } + } + + // body.last() is the closing `}`; insert return just before it + for (idx, bl) in body.iter().enumerate() { + if idx + 1 == body.len() { + result.push(format!(" return {};", out_name)); + } + result.push(bl.to_string()); + } + + continue; + } + } + + result.push(lines[i].to_string()); + i += 1; + } + + result.join("\n") +} + +/// Extract `(out_param_name, coord_param_name)` from a mainImage signature string. +fn extract_mainimage_params(sig: &str) -> Option<(String, String)> { + let paren_start = sig.find('(')?; + let paren_end = sig.rfind(')')?; + let params_str = &sig[paren_start + 1..paren_end]; + + let mut out_name = None; + let mut coord_name = None; + + for param in params_str.split(',') { + let words: Vec<&str> = param.split_whitespace().collect(); + match words.as_slice() { + ["out", "vec4", name] | ["out", "vec4", name, ..] => { + out_name = Some((*name).to_string()); + } + ["in", "vec2", name] | ["vec2", name] => { + coord_name = Some((*name).to_string()); + } + _ => {} + } + } + + Some((out_name?, coord_name.unwrap_or_else(|| "fragCoord".to_string()))) +} + +/// Strip function prototype declarations (e.g. `vec2 march(vec3, vec3);`). +/// naga does two-pass function collection so prototypes are not needed, +/// and some qualifier combinations (like `inout`) in prototypes confuse the parser. +fn strip_prototypes(src: &str) -> String { + let mut out = String::with_capacity(src.len()); + let mut depth: i32 = 0; + + for line in src.lines() { + let t = line.trim(); + let opens = line.chars().filter(|&c| c == '{').count() as i32; + let closes = line.chars().filter(|&c| c == '}').count() as i32; + + // Only strip at global scope and only lines that look like prototypes: + // end with `);`, contain a `(`, have 2+ words before the `(`. + if depth == 0 && is_prototype(t) { + // skip — don't push + } else { + out.push_str(line); + out.push('\n'); + } + + depth += opens - closes; + } + out +} + +fn is_prototype(t: &str) -> bool { + if !t.ends_with(");") { return false; } + if t.contains('{') || t.contains('=') { return false; } + let Some(paren) = t.find('(') else { return false }; + // Must have at least "type name(" before the opening paren + t[..paren].split_whitespace().count() >= 2 +} + +/// Remove the `void main() { … }` block from a GLSL source string. +/// Handles both `void main() {` (brace on signature line) and +/// `void main(void)` followed by `{` on the next line. +fn strip_void_main(src: &str) -> String { + let mut out = String::with_capacity(src.len()); + let mut in_main = false; + let mut found_open = false; + let mut depth: i32 = 0; + + for line in src.lines() { + if !in_main { + let t = line.trim(); + if t.starts_with("void main") && t.contains('(') { + in_main = true; + found_open = false; + depth = 0; + let opens = line.chars().filter(|&c| c == '{').count() as i32; + let closes = line.chars().filter(|&c| c == '}').count() as i32; + depth += opens - closes; + if opens > 0 { found_open = true; } + // Only finish immediately if the whole function is on one line. + if found_open && depth <= 0 { in_main = false; } + continue; + } + out.push_str(line); + out.push('\n'); + } else { + let opens = line.chars().filter(|&c| c == '{').count() as i32; + let closes = line.chars().filter(|&c| c == '}').count() as i32; + depth += opens - closes; + if opens > 0 { found_open = true; } + if found_open && depth <= 0 { in_main = false; } + // Don't push — stripping this block. + } + } + out +} + +/// Remove `#ifdef GL_ES … #endif` blocks and bare `precision …;` lines. +fn strip_gles_compat(src: &str) -> String { + let mut out = String::with_capacity(src.len()); + let mut in_block = false; + let mut depth: u32 = 0; + + for line in src.lines() { + let t = line.trim(); + if !in_block && t == "#ifdef GL_ES" { + in_block = true; + depth = 1; + continue; + } + if in_block { + if t.starts_with("#ifdef") || t.starts_with("#ifndef") { + depth += 1; + } else if t == "#endif" { + depth -= 1; + if depth == 0 { + in_block = false; + } + } + continue; + } + if t.starts_with("precision ") && t.ends_with(';') { + continue; + } + out.push_str(line); + out.push('\n'); + } + out +} + +/// Remove a line of the form `uniform ;`. +fn strip_uniform(src: &str, ty: &str, var: &str) -> String { + src.lines() + .map(|line| { + let t = line.trim(); + if t.starts_with("uniform") && t.contains(ty) && t.contains(var) { + String::new() + } else { + line.to_string() + } + }) + .collect::>() + .join("\n") +} diff --git a/src/wayland.rs b/src/wayland.rs new file mode 100644 index 0000000..153ce1a --- /dev/null +++ b/src/wayland.rs @@ -0,0 +1,285 @@ +//! Wayland state: output enumeration, layer surface creation, event dispatch. + +use std::time::{Duration, Instant}; + +use anyhow::{bail, Context, Result}; +use smithay_client_toolkit::{ + compositor::{CompositorHandler, CompositorState}, + delegate_compositor, delegate_layer, delegate_output, delegate_registry, + output::{OutputHandler, OutputState}, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + shell::{ + wlr_layer::{ + Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface, + LayerSurfaceConfigure, + }, + WaylandSurface, + }, +}; +use wayland_client::{ + globals::registry_queue_init, + protocol::{wl_output, wl_surface}, + Connection, Proxy, QueueHandle, +}; + +use crate::renderer::Renderer; +use crate::shader::ShaderSource; + +/// Application state held across the Wayland event loop. +pub struct AppState { + // SCTK required fields + registry_state: RegistryState, + output_state: OutputState, + + // Layer shell + layer: Option, + + // Rendering + renderer: Option, + + // Configuration that renderer needs at configure time + display_ptr: *mut std::ffi::c_void, + surface_ptr: *mut std::ffi::c_void, + shader: ShaderSource, + fps: u32, + + // Bookkeeping + configured: bool, + width: u32, + height: u32, + pub exit: bool, +} + +// Safety: we only use the raw pointers in single-threaded context +unsafe impl Send for AppState {} +unsafe impl Sync for AppState {} + +/// Parse the layer CLI string into an SCTK Layer. +pub fn parse_layer(s: &str) -> Result { + match s.to_lowercase().as_str() { + "background" => Ok(Layer::Background), + "bottom" => Ok(Layer::Bottom), + "top" => Ok(Layer::Top), + "overlay" => Ok(Layer::Overlay), + other => bail!("unknown layer: {}", other), + } +} + +/// Set up a Wayland connection and run the render loop for one output. +pub fn run( + target_output: String, + shader: ShaderSource, + fps: u32, + layer_kind: Layer, +) -> Result<()> { + let conn = Connection::connect_to_env().context("failed to connect to Wayland display")?; + let display_ptr = conn.backend().display_ptr() as *mut std::ffi::c_void; + + let (globals, mut event_queue) = + registry_queue_init::(&conn).context("registry_queue_init")?; + let qh = event_queue.handle(); + + let compositor = + CompositorState::bind(&globals, &qh).context("wl_compositor not available")?; + let layer_shell = + LayerShell::bind(&globals, &qh).context("zwlr_layer_shell_v1 not available")?; + + let mut state = AppState { + registry_state: RegistryState::new(&globals), + output_state: OutputState::new(&globals, &qh), + layer: None, + renderer: None, + display_ptr, + surface_ptr: std::ptr::null_mut(), + shader, + fps, + configured: false, + width: 0, + height: 0, + exit: false, + }; + + // One roundtrip to discover outputs. + event_queue.roundtrip(&mut state).context("initial roundtrip")?; + + // Find the matching wl_output. + let target_wl_output = find_output(&state.output_state, &target_output); + + // Create the wl_surface and layer surface. + let surface = compositor.create_surface(&qh); + state.surface_ptr = surface.id().as_ptr() as *mut std::ffi::c_void; + + let layer = layer_shell.create_layer_surface( + &qh, + surface, + layer_kind, + Some("gpupaper"), + target_wl_output.as_ref(), + ); + + // Stretch to fill the entire output. + layer.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT); + layer.set_exclusive_zone(-1); + layer.set_size(0, 0); + layer.set_keyboard_interactivity(KeyboardInteractivity::None); + layer.commit(); + state.layer = Some(layer); + + // Wait for the configure event which gives us the actual dimensions. + log::info!("waiting for configure…"); + while !state.configured { + event_queue.blocking_dispatch(&mut state).context("event dispatch failed")?; + } + + log::info!("configured: {}x{}", state.width, state.height); + + // Create renderer now that we have confirmed dimensions. + state.renderer = Some(unsafe { + Renderer::new( + state.display_ptr, + state.surface_ptr, + state.width, + state.height, + &state.shader, + state.fps, + ) + .context("failed to create renderer")? + }); + + // Render loop. + let frame_duration = if fps > 0 { + Some(Duration::from_nanos(1_000_000_000 / fps as u64)) + } else { + None + }; + + loop { + // Non-blocking Wayland event dispatch. + event_queue.dispatch_pending(&mut state).context("event dispatch")?; + // Flush outgoing messages. + conn.flush().ok(); + + if state.exit { + break; + } + + let frame_start = Instant::now(); + + if let Some(r) = state.renderer.as_mut() { + r.render(); + } + + // Cap frame rate if requested. + if let Some(dur) = frame_duration { + let elapsed = frame_start.elapsed(); + if elapsed < dur { + std::thread::sleep(dur - elapsed); + } + } + } + + Ok(()) +} + +/// Try to find the wl_output whose name matches `target`. +/// If `target` is `"all"` or `"*"`, returns `None` (compositor picks). +fn find_output(output_state: &OutputState, target: &str) -> Option { + if target == "all" || target == "*" { + return None; + } + for output in output_state.outputs() { + if let Some(info) = output_state.info(&output) { + if let Some(name) = &info.name { + if name == target { + log::info!("matched output: {}", name); + return Some(output); + } + } + } + } + log::warn!("output '{}' not found; using default", target); + None +} + +// --------------------------------------------------------------------------- +// Trait implementations for AppState +// --------------------------------------------------------------------------- + +impl CompositorHandler for AppState { + fn scale_factor_changed( + &mut self, _: &Connection, _: &QueueHandle, + _: &wl_surface::WlSurface, _: i32, + ) {} + + fn transform_changed( + &mut self, _: &Connection, _: &QueueHandle, + _: &wl_surface::WlSurface, _: wl_output::Transform, + ) {} + + fn frame( + &mut self, _: &Connection, _: &QueueHandle, + _: &wl_surface::WlSurface, _: u32, + ) {} + + fn surface_enter( + &mut self, _: &Connection, _: &QueueHandle, + _: &wl_surface::WlSurface, _: &wl_output::WlOutput, + ) {} + + fn surface_leave( + &mut self, _: &Connection, _: &QueueHandle, + _: &wl_surface::WlSurface, _: &wl_output::WlOutput, + ) {} +} + +impl OutputHandler for AppState { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + + fn new_output(&mut self, _: &Connection, _: &QueueHandle, _: wl_output::WlOutput) {} + fn update_output(&mut self, _: &Connection, _: &QueueHandle, _: wl_output::WlOutput) {} + fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle, _: wl_output::WlOutput) {} +} + +impl LayerShellHandler for AppState { + fn closed(&mut self, _: &Connection, _: &QueueHandle, _: &LayerSurface) { + log::info!("layer surface closed"); + self.exit = true; + } + + fn configure( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _layer: &LayerSurface, + configure: LayerSurfaceConfigure, + _serial: u32, + ) { + let (w, h) = configure.new_size; + self.width = if w == 0 { 1920 } else { w }; + self.height = if h == 0 { 1080 } else { h }; + + if !self.configured { + self.configured = true; + } else if let Some(r) = self.renderer.as_mut() { + r.resize(self.width, self.height); + } + + log::debug!("configure: {}x{}", self.width, self.height); + } +} + +// Delegation macros +delegate_compositor!(AppState); +delegate_output!(AppState); +delegate_layer!(AppState); +delegate_registry!(AppState); + +impl ProvidesRegistryState for AppState { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![OutputState]; +}