From c5668aee9f9b3d6a6d3fc621768b440a72b4c290 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Mon, 8 Jun 2026 21:16:22 +0200 Subject: [PATCH] multi-output support with shared GPU context, drop AUR check() Co-Authored-By: Claude Sonnet 4.6 --- aur/PKGBUILD | 6 - src/renderer.rs | 433 +++++++++++++++++++++++++--------------------- src/wayland.rs | 443 +++++++++++++++++++++++++++++++----------------- 3 files changed, 520 insertions(+), 362 deletions(-) diff --git a/aur/PKGBUILD b/aur/PKGBUILD index c5defda..bd8f57e 100644 --- a/aur/PKGBUILD +++ b/aur/PKGBUILD @@ -36,12 +36,6 @@ build() { cargo build --offline --release } -check() { - cd "$srcdir/gpupaper" - export RUSTUP_TOOLCHAIN=stable - cargo test --offline -} - package() { cd "$srcdir/gpupaper" diff --git a/src/renderer.rs b/src/renderer.rs index 03456b3..8141860 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,11 +1,16 @@ //! wgpu device, surface, pipeline setup and render loop. +use std::ptr::NonNull; use std::time::Instant; + use anyhow::{Context, Result}; use bytemuck::{Pod, Zeroable}; use raw_window_handle::{ HasDisplayHandle, RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, }; +use wgpu::util::DeviceExt; + +use crate::shader::ShaderSource; #[derive(Debug)] struct DisplayHandleWrapper(RawDisplayHandle); @@ -18,10 +23,6 @@ impl HasDisplayHandle for DisplayHandleWrapper { } 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)] @@ -33,21 +34,6 @@ pub struct Uniforms { 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, @@ -73,8 +59,6 @@ fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput { } "#; -/// 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; @@ -95,13 +79,137 @@ void main() { } "#; -impl Renderer { - /// Create a new renderer from raw Wayland display/surface pointers. +// --------------------------------------------------------------------------- +// GpuContext — one per process, shared across all outputs via Arc. +// --------------------------------------------------------------------------- + +/// Shared GPU state. Create once; pass `&GpuContext` into every +/// `SurfaceRenderer`. +pub struct GpuContext { + /// Kept alive so surfaces created later can use it. + pub(crate) instance: wgpu::Instance, + /// Needed by `SurfaceRenderer` to query surface capabilities. + pub(crate) adapter: wgpu::Adapter, + pub device: wgpu::Device, + pub queue: wgpu::Queue, + /// All outputs share one start time so shader animations stay in sync. + pub start: Instant, +} + +impl GpuContext { + /// Create the shared GPU context. + /// + /// Adapter selection is Vulkan-discrete → Vulkan-other → GL, with + /// `catch_unwind` to skip drivers that panic inside `vkCreateDevice` + /// (e.g. Mesa V3DV on Raspberry Pi). + pub unsafe fn new(display_ptr: *mut std::ffi::c_void) -> Result { + let raw_display = RawDisplayHandle::Wayland(WaylandDisplayHandle::new( + NonNull::new(display_ptr).context("null display pointer")?, + )); + + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::VULKAN | wgpu::Backends::GL, + flags: wgpu::InstanceFlags::default(), + memory_budget_thresholds: Default::default(), + backend_options: Default::default(), + display: Some(Box::new(DisplayHandleWrapper(raw_display))), + }); + + // Build priority-ordered candidate list without surface filtering — + // all Wayland surfaces on a single machine share the same GPU. + let all: Vec = + pollster::block_on(instance.enumerate_adapters(wgpu::Backends::all())); + + let mut vulkan: Vec<_> = all + .iter() + .filter(|a| a.get_info().backend == wgpu::Backend::Vulkan) + .cloned() + .collect(); + vulkan.sort_by_key(|a| { + (a.get_info().device_type != wgpu::DeviceType::DiscreteGpu) as u8 + }); + let gl: Vec<_> = all + .iter() + .filter(|a| a.get_info().backend == wgpu::Backend::Gl) + .cloned() + .collect(); + let candidates: Vec<_> = vulkan.into_iter().chain(gl).collect(); + + if candidates.is_empty() { + anyhow::bail!("no wgpu adapters found"); + } + + let device_desc = wgpu::DeviceDescriptor { + label: Some("gpupaper"), + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::downlevel_defaults(), + memory_hints: wgpu::MemoryHints::default(), + experimental_features: Default::default(), + trace: Default::default(), + }; + + let mut selected: Option<(wgpu::Adapter, wgpu::Device, wgpu::Queue)> = None; + for adapter in &candidates { + let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + pollster::block_on(adapter.request_device(&device_desc)) + })); + match outcome { + Ok(Ok((device, queue))) => { + selected = Some((adapter.clone(), device, queue)); + break; + } + Ok(Err(e)) => { + log::warn!( + "skipping adapter {:?}: request_device failed: {e}", + adapter.get_info().name + ); + } + Err(_) => { + log::warn!( + "skipping adapter {:?}: Vulkan driver panicked during device creation", + adapter.get_info().name + ); + } + } + } + + let (adapter, device, queue) = + selected.context("no adapter could create a wgpu device")?; + log::info!("wgpu adapter: {:?}", adapter.get_info()); + + Ok(GpuContext { + instance, + adapter, + device, + queue, + start: Instant::now(), + }) + } +} + +// --------------------------------------------------------------------------- +// SurfaceRenderer — one per output, borrows GpuContext for GPU ops. +// --------------------------------------------------------------------------- + +/// Per-output renderer. Multiple instances share a single `GpuContext`. +pub struct SurfaceRenderer { + surface: wgpu::Surface<'static>, + surface_config: wgpu::SurfaceConfiguration, + pipeline: wgpu::RenderPipeline, + uniform_buf: wgpu::Buffer, + bind_group: wgpu::BindGroup, + width: u32, + height: u32, +} + +impl SurfaceRenderer { + /// Create a surface renderer for one Wayland output. /// /// # Safety - /// The caller must guarantee that `display_ptr` and `surface_ptr` are - /// valid Wayland pointers that outlive this `Renderer`. + /// `display_ptr` and `surface_ptr` must be valid Wayland pointers that + /// outlive this `SurfaceRenderer`. pub unsafe fn new( + gpu: &GpuContext, display_ptr: *mut std::ffi::c_void, surface_ptr: *mut std::ffi::c_void, width: u32, @@ -116,100 +224,26 @@ impl Renderer { NonNull::new(surface_ptr).context("null surface pointer")?, )); - let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::VULKAN | 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 + let surface = gpu + .instance .create_surface_unsafe(wgpu::SurfaceTargetUnsafe::RawHandle { raw_display_handle: Some(raw_display), raw_window_handle: raw_window, }) .context("failed to create wgpu surface")?; - // Build a priority-ordered candidate list: Vulkan discrete first, then - // Vulkan integrated, then GL. We try each in turn and fall back on - // failure so that Vulkan is used wherever it works (NVIDIA, AMD, etc.) - // while broken Vulkan drivers (e.g. Mesa V3DV on Raspberry Pi, which - // advertises features it can't actually enable) are skipped silently. - let mut candidates: Vec = { - let all: Vec = pollster::block_on( - instance.enumerate_adapters(wgpu::Backends::all()), - ) - .into_iter() - .filter(|a| a.is_surface_supported(&wgpu_surface)) - .collect(); + Self::from_surface(gpu, surface, width, height, shader, fps) + } - let mut vulkan: Vec<_> = all - .iter() - .filter(|a| a.get_info().backend == wgpu::Backend::Vulkan) - .cloned() - .collect(); - // Discrete GPUs first within the Vulkan set. - vulkan.sort_by_key(|a| { - (a.get_info().device_type != wgpu::DeviceType::DiscreteGpu) as u8 - }); - - let gl: Vec<_> = all - .iter() - .filter(|a| a.get_info().backend == wgpu::Backend::Gl) - .cloned() - .collect(); - - vulkan.into_iter().chain(gl).collect() - }; - - if candidates.is_empty() { - anyhow::bail!("no suitable wgpu adapter found"); - } - - let device_desc = wgpu::DeviceDescriptor { - label: Some("gpupaper"), - required_features: wgpu::Features::empty(), - // downlevel_defaults is safe on both GL/GLES and Vulkan. - required_limits: wgpu::Limits::downlevel_defaults(), - memory_hints: wgpu::MemoryHints::default(), - experimental_features: Default::default(), - trace: Default::default(), - }; - - // Try each candidate. Some Vulkan drivers (e.g. Mesa V3DV) panic - // inside vkCreateDevice with VK_ERROR_FEATURE_NOT_PRESENT despite - // advertising the feature — catch that and move to the next adapter. - let (adapter, device, queue) = candidates - .drain(..) - .find_map(|adapter| { - let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - pollster::block_on(adapter.request_device(&device_desc)) - })); - match outcome { - Ok(Ok((device, queue))) => Some((adapter, device, queue)), - Ok(Err(e)) => { - log::warn!( - "skipping adapter {:?}: request_device failed: {e}", - adapter.get_info().name - ); - None - } - Err(_) => { - log::warn!( - "skipping adapter {:?}: Vulkan driver panicked during device creation", - adapter.get_info().name - ); - None - } - } - }) - .context("no adapter could create a wgpu device")?; - - log::info!("wgpu adapter: {:?}", adapter.get_info()); - - // Surface configuration - let caps = wgpu_surface.get_capabilities(&adapter); + fn from_surface( + gpu: &GpuContext, + surface: wgpu::Surface<'static>, + width: u32, + height: u32, + shader: &ShaderSource, + fps: u32, + ) -> Result { + let caps = surface.get_capabilities(&gpu.adapter); let format = caps .formats .iter() @@ -217,11 +251,12 @@ impl Renderer { .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 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, @@ -233,36 +268,39 @@ impl Renderer { desired_maximum_frame_latency: 2, present_mode, }; - wgpu_surface.configure(&device, &surface_config); + surface.configure(&gpu.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 uniform_buf = gpu + .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_layout = + gpu.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 { + let bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("uniforms bind group"), layout: &bind_group_layout, entries: &[wgpu::BindGroupEntry { @@ -271,25 +309,27 @@ impl Renderer { }], }); - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("pipeline layout"), - bind_group_layouts: &[Some(&bind_group_layout)], - immediate_size: 0, - }); + let pipeline_layout = + gpu.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 { + gpu.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 { + gpu.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 { + gpu.device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("vertex shader (glsl)"), source: wgpu::ShaderSource::Glsl { shader: VERTEX_SHADER_GLSL.into(), @@ -297,7 +337,7 @@ impl Renderer { defines: &[], }, }), - device.create_shader_module(wgpu::ShaderModuleDescriptor { + gpu.device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("fragment shader (glsl)"), source: wgpu::ShaderSource::Glsl { shader: src.as_str().into(), @@ -308,51 +348,49 @@ impl Renderer { ), }; - 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, - }); + let pipeline = + gpu.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, + Ok(SurfaceRenderer { + 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) { + pub fn resize(&mut self, gpu: &GpuContext, width: u32, height: u32) { if width == 0 || height == 0 { return; } @@ -360,44 +398,45 @@ impl Renderer { self.height = height; self.surface_config.width = width; self.surface_config.height = height; - self.surface.configure(&self.device, &self.surface_config); + self.surface.configure(&gpu.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(); + pub fn render(&mut self, gpu: &GpuContext, elapsed: f32) -> bool { 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)); + gpu.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); + self.surface.configure(&gpu.device, &self.surface_config); t } wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Outdated => { - self.surface.configure(&self.device, &self.surface_config); + self.surface.configure(&gpu.device, &self.surface_config); return false; } wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded - | wgpu::CurrentSurfaceTexture::Validation => { - return false; - } + | 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"), - }); + surface_texture + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = + gpu.device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("frame encoder"), + }); { let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -421,7 +460,7 @@ impl Renderer { rpass.draw(0..3, 0..1); } - self.queue.submit(Some(encoder.finish())); + gpu.queue.submit(Some(encoder.finish())); surface_texture.present(); true } diff --git a/src/wayland.rs b/src/wayland.rs index 448d4f6..27ecacf 100644 --- a/src/wayland.rs +++ b/src/wayland.rs @@ -1,5 +1,6 @@ //! Wayland state: output enumeration, layer surface creation, event dispatch. +use std::sync::Arc; use std::time::{Duration, Instant}; use anyhow::{bail, Context, Result}; @@ -23,43 +24,94 @@ use wayland_client::{ Connection, Proxy, QueueHandle, }; -use crate::renderer::Renderer; +use crate::renderer::{GpuContext, SurfaceRenderer}; use crate::shader::ShaderSource; -/// Application state held across the Wayland event loop. -pub struct AppState { - // SCTK required fields - registry_state: RegistryState, - output_state: OutputState, +// --------------------------------------------------------------------------- +// Per-output state +// --------------------------------------------------------------------------- - // Layer shell - layer: Option, - - // Rendering - renderer: Option, - - // Configuration that renderer needs at configure time - display_ptr: *mut std::ffi::c_void, +struct OutputSurface { + layer: LayerSurface, + renderer: Option, + /// Raw wl_surface pointer passed to SurfaceRenderer. surface_ptr: *mut std::ffi::c_void, - shader: ShaderSource, - fps: u32, - - // Bookkeeping - configured: bool, width: u32, height: u32, - pub exit: bool, - - // Frame callback: set true when compositor says it's ready for next frame draw_ready: bool, last_frame: Instant, } -// Safety: we only use the raw pointers in single-threaded context +// Safety: raw pointer is only used from the single Wayland thread. +unsafe impl Send for OutputSurface {} +unsafe impl Sync for OutputSurface {} + +// --------------------------------------------------------------------------- +// Application state +// --------------------------------------------------------------------------- + +pub struct AppState { + registry_state: RegistryState, + output_state: OutputState, + compositor: CompositorState, + layer_shell: LayerShell, + + outputs: Vec, + /// Created on first configure; shared by all SurfaceRenderers. + gpu: Option>, + + display_ptr: *mut std::ffi::c_void, + shader: ShaderSource, + fps: u32, + layer_kind: Layer, + /// "all" / "*" → create a surface per output; otherwise match by name. + target_output: String, + + frame_duration: Option, + pub exit: bool, +} + unsafe impl Send for AppState {} unsafe impl Sync for AppState {} -/// Parse the layer CLI string into an SCTK Layer. +// --------------------------------------------------------------------------- +// Layer creation helper (also called from new_output for hotplug) +// --------------------------------------------------------------------------- + +impl AppState { + fn attach_output(&mut self, qh: &QueueHandle, wl_out: Option<&wl_output::WlOutput>) { + let surface = self.compositor.create_surface(qh); + let surface_ptr = surface.id().as_ptr() as *mut std::ffi::c_void; + + let layer = self.layer_shell.create_layer_surface( + qh, + surface, + self.layer_kind, + Some("gpupaper"), + wl_out, + ); + 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(); + + self.outputs.push(OutputSurface { + layer, + renderer: None, + surface_ptr, + width: 0, + height: 0, + draw_ready: false, + last_frame: Instant::now(), + }); + } +} + +// --------------------------------------------------------------------------- +// Public entry point +// --------------------------------------------------------------------------- + pub fn parse_layer(s: &str) -> Result { match s.to_lowercase().as_str() { "background" => Ok(Layer::Background), @@ -70,7 +122,6 @@ pub fn parse_layer(s: &str) -> Result { } } -/// Set up a Wayland connection and run the render loop for one output. pub fn run( target_output: String, shader: ShaderSource, @@ -89,127 +140,116 @@ pub fn run( 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, - draw_ready: false, - last_frame: Instant::now(), - }; - - // 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 driven by wl_surface.frame callbacks. - // The compositor stops sending callbacks when the surface is fully occluded, - // so the loop naturally idles to zero CPU when another app is fullscreen. let frame_duration = if fps > 0 { Some(Duration::from_nanos(1_000_000_000 / fps as u64)) } else { None }; - // Register the first frame callback then render immediately. On Wayland - // the compositor only fires frame callbacks for commits that contain a - // buffer; an empty commit would stall the loop forever on many compositors. - // Rendering here attaches the first buffer and commits it together with the - // frame callback so the compositor has something to display right away. - { - let wl_surface = state.layer.as_ref().unwrap().wl_surface(); - wl_surface.frame(&qh, wl_surface.clone()); + let mut state = AppState { + registry_state: RegistryState::new(&globals), + output_state: OutputState::new(&globals, &qh), + compositor, + layer_shell, + outputs: Vec::new(), + gpu: None, + display_ptr, + shader, + fps, + layer_kind, + target_output: target_output.clone(), + frame_duration, + exit: false, + }; + + // Discover available outputs. + event_queue.roundtrip(&mut state).context("initial roundtrip")?; + + let is_all = target_output == "all" || target_output == "*"; + + if is_all { + let wl_outputs: Vec<_> = state.output_state.outputs().collect(); + if wl_outputs.is_empty() { + // No outputs reported yet; let the compositor assign. + state.attach_output(&qh, None); + } else { + for wl_out in &wl_outputs { + state.attach_output(&qh, Some(wl_out)); + } + } + } else { + let wl_out = find_output(&state.output_state, &target_output); + state.attach_output(&qh, wl_out.as_ref()); } - if let Some(r) = state.renderer.as_mut() { - r.render(); + + // Wait for every surface to receive its configure (and thus create a renderer). + log::info!("waiting for {} surface(s) to configure…", state.outputs.len()); + while state.outputs.iter().any(|o| o.renderer.is_none()) { + event_queue + .blocking_dispatch(&mut state) + .context("event dispatch failed")?; + + if state.exit { + return Ok(()); + } + } + log::info!("{} surface(s) ready", state.outputs.len()); + + // Render the first frame on each surface immediately so the compositor has + // a buffer and will start sending frame callbacks. + if let Some(gpu) = state.gpu.as_ref().map(Arc::clone) { + let elapsed = gpu.start.elapsed().as_secs_f32(); + for output in &mut state.outputs { + let wl_surface = output.layer.wl_surface(); + wl_surface.frame(&qh, wl_surface.clone()); + if let Some(r) = &mut output.renderer { + r.render(&gpu, elapsed); + } + } } conn.flush().ok(); + // Main event loop — each output is driven by its own frame callbacks so + // monitors at different refresh rates pace themselves independently. loop { - // Block the thread until the compositor sends us any event (including - // the frame callback). Zero CPU while occluded. - event_queue.blocking_dispatch(&mut state).context("event dispatch")?; + event_queue + .blocking_dispatch(&mut state) + .context("event dispatch")?; conn.flush().ok(); if state.exit { break; } - if !state.draw_ready { - continue; - } - state.draw_ready = false; + let gpu = match state.gpu.as_ref().map(Arc::clone) { + Some(g) => g, + None => continue, + }; + let elapsed = gpu.start.elapsed().as_secs_f32(); + let frame_duration = state.frame_duration; - // Optional fps cap: sleep the remainder of the frame budget. - if let Some(dur) = frame_duration { - let elapsed = state.last_frame.elapsed(); - if elapsed < dur { - std::thread::sleep(dur - elapsed); + for output in &mut state.outputs { + if !output.draw_ready { + continue; } - } - state.last_frame = Instant::now(); + output.draw_ready = false; - // Register the NEXT frame callback before present() so that the - // request is included in the same wl_surface.commit that wgpu - // issues inside present(). - { - let wl_surface = state.layer.as_ref().unwrap().wl_surface(); - wl_surface.frame(&qh, wl_surface.clone()); - } + if let Some(dur) = frame_duration { + let since_last = output.last_frame.elapsed(); + if since_last < dur { + std::thread::sleep(dur - since_last); + } + } + output.last_frame = Instant::now(); - if let Some(r) = state.renderer.as_mut() { - r.render(); + { + let wl_surface = output.layer.wl_surface(); + wl_surface.frame(&qh, wl_surface.clone()); + } + + if let Some(r) = &mut output.renderer { + r.render(&gpu, elapsed); + } } conn.flush().ok(); @@ -218,12 +258,7 @@ pub fn run( 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 { @@ -239,36 +274,61 @@ fn find_output(output_state: &OutputState, target: &str) -> Option, - _: &wl_surface::WlSurface, _: i32, - ) {} + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_surface::WlSurface, + _: i32, + ) { + } fn transform_changed( - &mut self, _: &Connection, _: &QueueHandle, - _: &wl_surface::WlSurface, _: wl_output::Transform, - ) {} + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_surface::WlSurface, + _: wl_output::Transform, + ) { + } fn frame( - &mut self, _: &Connection, _: &QueueHandle, - _: &wl_surface::WlSurface, _: u32, + &mut self, + _: &Connection, + _: &QueueHandle, + surface: &wl_surface::WlSurface, + _: u32, ) { - self.draw_ready = true; + let id = surface.id(); + for output in &mut self.outputs { + if output.layer.wl_surface().id() == id { + output.draw_ready = true; + break; + } + } } fn surface_enter( - &mut self, _: &Connection, _: &QueueHandle, - _: &wl_surface::WlSurface, _: &wl_output::WlOutput, - ) {} + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_surface::WlSurface, + _: &wl_output::WlOutput, + ) { + } fn surface_leave( - &mut self, _: &Connection, _: &QueueHandle, - _: &wl_surface::WlSurface, _: &wl_output::WlOutput, - ) {} + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wl_surface::WlSurface, + _: &wl_output::WlOutput, + ) { + } } impl OutputHandler for AppState { @@ -276,9 +336,35 @@ impl OutputHandler for AppState { &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) {} + fn new_output( + &mut self, + _conn: &Connection, + qh: &QueueHandle, + output: wl_output::WlOutput, + ) { + if self.target_output == "all" || self.target_output == "*" { + self.attach_output(qh, Some(&output)); + } + } + + fn update_output( + &mut self, + _: &Connection, + _: &QueueHandle, + _: wl_output::WlOutput, + ) { + } + + fn output_destroyed( + &mut self, + _: &Connection, + _: &QueueHandle, + output: wl_output::WlOutput, + ) { + // Drop the surface renderer for the disconnected output. + self.outputs + .retain(|o| o.layer.wl_surface().id() != output.id()); + } } impl LayerShellHandler for AppState { @@ -291,25 +377,64 @@ impl LayerShellHandler for AppState { &mut self, _conn: &Connection, _qh: &QueueHandle, - _layer: &LayerSurface, + 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 }; + let layer_id = layer.wl_surface().id(); + let Some(idx) = self.outputs.iter().position(|o| o.layer.wl_surface().id() == layer_id) + else { + return; + }; - if !self.configured { - self.configured = true; - } else if let Some(r) = self.renderer.as_mut() { - r.resize(self.width, self.height); + let (w, h) = configure.new_size; + let w = if w == 0 { 1920 } else { w }; + let h = if h == 0 { 1080 } else { h }; + self.outputs[idx].width = w; + self.outputs[idx].height = h; + + if self.outputs[idx].renderer.is_some() { + // Subsequent configure — just resize. + let gpu = self.gpu.as_ref().map(Arc::clone); + if let Some(gpu) = gpu { + if let Some(r) = self.outputs[idx].renderer.as_mut() { + r.resize(&gpu, w, h); + } + } + return; } - log::debug!("configure: {}x{}", self.width, self.height); + // First configure for this surface — create the GPU context if needed. + if self.gpu.is_none() { + match unsafe { GpuContext::new(self.display_ptr) } { + Ok(gpu) => self.gpu = Some(Arc::new(gpu)), + Err(e) => { + log::error!("failed to create GPU context: {e}"); + self.exit = true; + return; + } + } + } + + let gpu = Arc::clone(self.gpu.as_ref().unwrap()); + let surface_ptr = self.outputs[idx].surface_ptr; + let display_ptr = self.display_ptr; + let shader = &self.shader; + let fps = self.fps; + + match unsafe { SurfaceRenderer::new(&gpu, display_ptr, surface_ptr, w, h, shader, fps) } { + Ok(renderer) => { + self.outputs[idx].renderer = Some(renderer); + log::info!("output {} configured: {}x{}", idx, w, h); + } + Err(e) => { + log::error!("failed to create surface renderer for output {idx}: {e}"); + self.exit = true; + } + } } } -// Delegation macros delegate_compositor!(AppState); delegate_output!(AppState); delegate_layer!(AppState);