diff --git a/Cargo.lock b/Cargo.lock index ce68a68..c2742ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,7 +169,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2", + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", ] [[package]] @@ -368,6 +377,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + [[package]] name = "cursor-icon" version = "1.2.0" @@ -390,6 +410,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] + [[package]] name = "dlib" version = "0.5.3" @@ -902,6 +934,18 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -958,6 +1002,15 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-app-kit" version = "0.2.2" @@ -965,9 +1018,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.11.0", - "block2", + "block2 0.5.1", "libc", - "objc2", + "objc2 0.5.2", "objc2-core-data", "objc2-core-image", "objc2-foundation", @@ -981,8 +1034,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.11.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-core-location", "objc2-foundation", ] @@ -993,8 +1046,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-foundation", ] @@ -1005,8 +1058,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.11.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-foundation", ] @@ -1016,8 +1069,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-foundation", "objc2-metal", ] @@ -1028,8 +1081,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-contacts", "objc2-foundation", ] @@ -1047,10 +1100,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.11.0", - "block2", + "block2 0.5.1", "dispatch", "libc", - "objc2", + "objc2 0.5.2", ] [[package]] @@ -1059,8 +1112,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-app-kit", "objc2-foundation", ] @@ -1072,8 +1125,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.11.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-foundation", ] @@ -1084,8 +1137,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.11.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-foundation", "objc2-metal", ] @@ -1096,7 +1149,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ - "objc2", + "objc2 0.5.2", "objc2-foundation", ] @@ -1107,8 +1160,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.11.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", "objc2-core-image", @@ -1127,8 +1180,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-foundation", ] @@ -1139,8 +1192,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.11.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-core-location", "objc2-foundation", ] @@ -1176,6 +1229,12 @@ version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1208,6 +1267,24 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pixman" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea217d496c19ac0a8e502b37078e1f683d16344adee9eb247a5d57c165e1edf" +dependencies = [ + "drm-fourcc", + "paste", + "pixman-sys", + "thiserror 1.0.69", +] + +[[package]] +name = "pixman-sys" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0483e89e81d7915defe83c51f23f6800594d64f6f4a21253ce87fd8444ada" + [[package]] name = "pkg-config" version = "0.3.32" @@ -1593,6 +1670,7 @@ dependencies = [ "indexmap", "libc", "libloading", + "pixman", "profiling", "rand", "rustix 1.1.4", @@ -2331,7 +2409,7 @@ dependencies = [ "android-activity", "atomic-waker", "bitflags 2.11.0", - "block2", + "block2 0.5.1", "bytemuck", "calloop 0.13.0", "cfg_aliases", @@ -2344,7 +2422,7 @@ dependencies = [ "libc", "memmap2", "ndk", - "objc2", + "objc2 0.5.2", "objc2-app-kit", "objc2-foundation", "objc2-ui-kit", @@ -2491,6 +2569,7 @@ dependencies = [ name = "wrsrvd" version = "0.1.0" dependencies = [ + "ctrlc", "miette", "smithay", "thiserror 2.0.18", diff --git a/crates/wrsrvd/Cargo.toml b/crates/wrsrvd/Cargo.toml index 4208053..ca5ec7e 100644 --- a/crates/wrsrvd/Cargo.toml +++ b/crates/wrsrvd/Cargo.toml @@ -15,5 +15,7 @@ smithay = { version = "0.7", default-features = false, features = [ "wayland_frontend", "desktop", "renderer_gl", + "renderer_pixman", "backend_winit", ] } +ctrlc = "3" diff --git a/crates/wrsrvd/src/backend/headless.rs b/crates/wrsrvd/src/backend/headless.rs new file mode 100644 index 0000000..5b7074e --- /dev/null +++ b/crates/wrsrvd/src/backend/headless.rs @@ -0,0 +1,240 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use miette::Result; +use smithay::{ + backend::{ + allocator::Fourcc, + renderer::{ + Bind, ExportMem, Offscreen, + damage::OutputDamageTracker, + element::texture::TextureRenderElement, + pixman::{PixmanRenderer, PixmanTexture}, + }, + }, + desktop::{Window, space::render_output}, + output::Output, + reexports::pixman as pixman_lib, + reexports::{ + calloop::{self, EventLoop}, + wayland_server::Display, + }, + utils::{Buffer as BufferCoord, Rectangle, Size}, + wayland::{compositor::CompositorClientState, socket::ListeningSocketSource}, +}; +use tracing::{info, warn}; + +use crate::errors::WayRayError; +use crate::handlers::ClientState; +use crate::state::WayRay; + +/// Dark grey clear color for the compositor background. +const CLEAR_COLOR: [f32; 4] = [0.1, 0.1, 0.1, 1.0]; + +/// Data accessible from calloop event callbacks in the headless backend. +struct CalloopData { + state: WayRay, + display: Display, + renderer: PixmanRenderer, + render_buffer: pixman_lib::Image<'static, 'static>, + damage_tracker: OutputDamageTracker, +} + +/// Run the compositor with the headless PixmanRenderer backend. +/// +/// This creates a CPU-only software renderer suitable for headless servers +/// with no display hardware. +pub fn run(display: Display, mut state: WayRay, output: Output) -> Result<()> { + // Create the PixmanRenderer (CPU software renderer). + let mut renderer = PixmanRenderer::new().map_err(|e| { + WayRayError::BackendInit(Box::::from( + e.to_string(), + )) + })?; + info!("pixman headless backend initialized"); + + // Create an in-memory render buffer matching the output size. + let output_size = output.current_mode().unwrap().size; + let buffer_size: Size = Size::from((output_size.w, output_size.h)); + let render_buffer: pixman_lib::Image<'static, 'static> = renderer + .create_buffer(Fourcc::Argb8888, buffer_size) + .map_err(|e| { + WayRayError::BackendInit(Box::::from( + e.to_string(), + )) + })?; + info!( + width = output_size.w, + height = output_size.h, + "headless render buffer created" + ); + + // Create a Wayland listening socket for clients. + let listening_socket = + ListeningSocketSource::new_auto().map_err(|e| WayRayError::DisplayInit(Box::new(e)))?; + let socket_name = listening_socket.socket_name().to_os_string(); + info!(?socket_name, "wayland socket created"); + + // Set WAYLAND_DISPLAY so child processes can find us. + // SAFETY: This is called early in main before any other threads are spawned, + // so there are no concurrent readers of the environment. + unsafe { std::env::set_var("WAYLAND_DISPLAY", &socket_name) }; + + // Create the calloop event loop. + let mut event_loop: EventLoop = + EventLoop::try_new().map_err(|e| WayRayError::EventLoop(Box::new(e)))?; + + let loop_handle = event_loop.handle(); + + // Insert the Wayland listening socket as a calloop source. + loop_handle + .insert_source(listening_socket, |client_stream, _, data| { + data.display + .handle() + .insert_client( + client_stream, + Arc::new(ClientState { + compositor_state: CompositorClientState::default(), + }), + ) + .ok(); + }) + .map_err(|e| WayRayError::EventLoop(Box::new(e.error)))?; + + // Set up a timer to drive the render loop at ~60fps. + let timer = calloop::timer::Timer::from_duration(Duration::from_millis(16)); + loop_handle + .insert_source(timer, |_, _, data| { + render_headless_frame(data); + calloop::timer::TimeoutAction::ToDuration(Duration::from_millis(16)) + }) + .map_err(|e| WayRayError::EventLoop(Box::new(e.error)))?; + + // Create a damage tracker for efficient rendering. + let damage_tracker = OutputDamageTracker::from_output(&output); + + // Map the output into the compositor space. + state.space.map_output(&output, (0, 0)); + + let mut calloop_data = CalloopData { + state, + display, + renderer, + render_buffer, + damage_tracker, + }; + + let running = Arc::new(AtomicBool::new(true)); + + // Handle SIGINT/SIGTERM for graceful shutdown. + let running_clone = running.clone(); + ctrlc_handler(&running_clone); + + info!("entering headless main event loop"); + + while running.load(Ordering::SeqCst) { + // Dispatch Wayland clients. + calloop_data + .display + .dispatch_clients(&mut calloop_data.state) + .map_err(|e| WayRayError::EventLoop(Box::new(e)))?; + + calloop_data + .display + .flush_clients() + .map_err(|e| WayRayError::EventLoop(Box::new(e)))?; + + // Dispatch calloop sources (timer + Wayland socket) with ~16ms timeout. + event_loop + .dispatch(Duration::from_millis(16), &mut calloop_data) + .map_err(|e| WayRayError::EventLoop(Box::new(e)))?; + } + + info!("headless backend shutting down"); + Ok(()) +} + +/// Render a single frame using the headless PixmanRenderer. +fn render_headless_frame(data: &mut CalloopData) { + let output = data.state.output.clone(); + + // Bind the in-memory buffer as the render target. + let mut target = match data.renderer.bind(&mut data.render_buffer) { + Ok(target) => target, + Err(err) => { + warn!(?err, "failed to bind headless render buffer"); + return; + } + }; + + let custom_elements: &[TextureRenderElement] = &[]; + + let render_result = render_output::<_, _, Window, _>( + &output, + &mut data.renderer, + &mut target, + 1.0, + 0, // buffer age: 0 means full redraw (no swap chain in headless) + [&data.state.space], + custom_elements, + &mut data.damage_tracker, + CLEAR_COLOR, + ); + + match render_result { + Ok(result) => { + let damage = result.damage.cloned(); + + // Read pixels from the CPU buffer for network transport. + let output_size = data.state.output.current_mode().unwrap().size; + let region: Rectangle = + Rectangle::from_size(Size::from((output_size.w, output_size.h))); + + match data + .renderer + .copy_framebuffer(&target, region, Fourcc::Argb8888) + { + Ok(mapping) => match data.renderer.map_texture(&mapping) { + Ok(pixels) => { + let damage_rects = damage.as_ref().map(|d| d.len()).unwrap_or(0); + tracing::debug!( + width = output_size.w, + height = output_size.h, + bytes = pixels.len(), + damage_rects, + "headless framebuffer captured" + ); + } + Err(err) => { + tracing::warn!(?err, "failed to map headless framebuffer"); + } + }, + Err(err) => { + tracing::warn!(?err, "failed to copy headless framebuffer"); + } + } + + // Send frame callbacks to all mapped surfaces. + let time = data.state.clock.now(); + for window in data.state.space.elements() { + window.send_frame(&output, time, Some(Duration::ZERO), |_, _| { + Some(output.clone()) + }); + } + } + Err(err) => { + warn!(?err, "headless damage tracker render failed"); + } + } +} + +/// Install a Ctrl-C handler that sets the running flag to false. +fn ctrlc_handler(running: &Arc) { + let r = running.clone(); + // Ignore error if handler can't be set (e.g., in tests). + let _ = ctrlc::set_handler(move || { + info!("received shutdown signal"); + r.store(false, Ordering::SeqCst); + }); +} diff --git a/crates/wrsrvd/src/backend/mod.rs b/crates/wrsrvd/src/backend/mod.rs new file mode 100644 index 0000000..cc833c7 --- /dev/null +++ b/crates/wrsrvd/src/backend/mod.rs @@ -0,0 +1,2 @@ +pub mod headless; +pub mod winit; diff --git a/crates/wrsrvd/src/backend/winit.rs b/crates/wrsrvd/src/backend/winit.rs new file mode 100644 index 0000000..e603203 --- /dev/null +++ b/crates/wrsrvd/src/backend/winit.rs @@ -0,0 +1,259 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use miette::Result; +use smithay::{ + backend::{ + allocator::Fourcc, + renderer::{ + ExportMem, + damage::OutputDamageTracker, + element::texture::TextureRenderElement, + gles::{GlesRenderer, GlesTexture}, + }, + winit::{self, WinitEvent, WinitGraphicsBackend}, + }, + desktop::{Window, space::render_output}, + output::Output, + reexports::wayland_server::Display, + utils::{Buffer as BufferCoord, Rectangle, Size}, + wayland::{compositor::CompositorClientState, socket::ListeningSocketSource}, +}; +use tracing::{info, warn}; + +use crate::errors::WayRayError; +use crate::handlers::ClientState; +use crate::state::WayRay; + +/// Dark grey clear color for the compositor background. +const CLEAR_COLOR: [f32; 4] = [0.1, 0.1, 0.1, 1.0]; + +/// Data accessible from calloop event callbacks in the Winit backend. +struct CalloopData { + state: WayRay, + display: Display, + backend: WinitGraphicsBackend, + damage_tracker: OutputDamageTracker, +} + +/// Run the compositor with the Winit backend (opens a window for development). +pub fn run(display: Display, mut state: WayRay, output: Output) -> Result<()> { + // Initialize the Winit backend (opens a window, creates a GlesRenderer). + let (backend, winit_event_loop) = winit::init::().map_err(|e| { + WayRayError::BackendInit(Box::::from( + e.to_string(), + )) + })?; + info!("winit backend initialized"); + + // Update the output mode to match the actual Winit window size. + let window_size = backend.window_size(); + let mode = smithay::output::Mode { + size: window_size, + refresh: 60_000, + }; + output.change_current_state(Some(mode), None, None, None); + output.set_preferred(mode); + info!(?window_size, "output mode updated to match winit window"); + + // Create a Wayland listening socket for clients. + let listening_socket = + ListeningSocketSource::new_auto().map_err(|e| WayRayError::DisplayInit(Box::new(e)))?; + let socket_name = listening_socket.socket_name().to_os_string(); + info!(?socket_name, "wayland socket created"); + + // Set WAYLAND_DISPLAY so child processes can find us. + // SAFETY: This is called early in main before any other threads are spawned, + // so there are no concurrent readers of the environment. + unsafe { std::env::set_var("WAYLAND_DISPLAY", &socket_name) }; + + // Create the calloop event loop. + let mut event_loop: smithay::reexports::calloop::EventLoop = + smithay::reexports::calloop::EventLoop::try_new() + .map_err(|e| WayRayError::EventLoop(Box::new(e)))?; + + let loop_handle = event_loop.handle(); + + // Insert the Wayland listening socket as a calloop source. + loop_handle + .insert_source(listening_socket, |client_stream, _, data| { + data.display + .handle() + .insert_client( + client_stream, + Arc::new(ClientState { + compositor_state: CompositorClientState::default(), + }), + ) + .ok(); + }) + .map_err(|e| WayRayError::EventLoop(Box::new(e.error)))?; + + // Shared flag to signal the main loop to exit. + let running = Arc::new(AtomicBool::new(true)); + let running_clone = running.clone(); + + // Insert the Winit event loop as a calloop source. + loop_handle + .insert_source(winit_event_loop, move |event, _, data| match event { + WinitEvent::Resized { size, scale_factor } => { + info!(?size, scale_factor, "window resized"); + } + WinitEvent::Focus(focused) => { + info!(focused, "window focus changed"); + } + WinitEvent::Input(event) => { + data.state.process_input_event(event); + } + WinitEvent::Redraw => { + render_winit_frame(&mut data.state, &mut data.backend, &mut data.damage_tracker); + } + WinitEvent::CloseRequested => { + info!("close requested, shutting down"); + running_clone.store(false, Ordering::SeqCst); + } + }) + .map_err(|e| WayRayError::EventLoop(Box::new(e.error)))?; + + // Create a damage tracker for efficient rendering. + let damage_tracker = OutputDamageTracker::from_output(&output); + + // Map the output into the compositor space. + state.space.map_output(&output, (0, 0)); + + let mut calloop_data = CalloopData { + state, + display, + backend, + damage_tracker, + }; + + info!("entering winit main event loop"); + + while running.load(Ordering::SeqCst) { + // Dispatch Wayland clients. + calloop_data + .display + .dispatch_clients(&mut calloop_data.state) + .map_err(|e| WayRayError::EventLoop(Box::new(e)))?; + + calloop_data + .display + .flush_clients() + .map_err(|e| WayRayError::EventLoop(Box::new(e)))?; + + // Dispatch calloop sources (Winit events + Wayland socket) with ~16ms timeout. + event_loop + .dispatch(Duration::from_millis(16), &mut calloop_data) + .map_err(|e| WayRayError::EventLoop(Box::new(e)))?; + } + + info!("winit backend shutting down"); + Ok(()) +} + +/// Render the compositor space to the Winit backend window. +/// +/// Uses `OutputDamageTracker` for efficient re-rendering: only +/// damaged regions are redrawn each frame. +fn render_winit_frame( + state: &mut WayRay, + backend: &mut WinitGraphicsBackend, + damage_tracker: &mut OutputDamageTracker, +) { + let output = state.output.clone(); + + // Get buffer age before bind (avoids borrow conflict). + let age = backend.buffer_age().unwrap_or(0); + + // Render within a block so framebuffer is dropped before submit. + let render_result = { + let (renderer, mut framebuffer) = match backend.bind() { + Ok(pair) => pair, + Err(err) => { + warn!(?err, "failed to bind winit backend for rendering"); + return; + } + }; + + let custom_elements: &[TextureRenderElement] = &[]; + + let render_result = render_output::<_, _, Window, _>( + &output, + renderer, + &mut framebuffer, + 1.0, + age, + [&state.space], + custom_elements, + damage_tracker, + CLEAR_COLOR, + ); + + match render_result { + Ok(result) => { + let damage = result.damage.cloned(); + + // Verify framebuffer capture path works (will be consumed + // by network transport in Phase 1). + let output_size = state.output.current_mode().unwrap().size; + let region: Rectangle = + Rectangle::from_size(Size::from((output_size.w, output_size.h))); + + match renderer.copy_framebuffer(&framebuffer, region, Fourcc::Argb8888) { + Ok(mapping) => match renderer.map_texture(&mapping) { + Ok(pixels) => { + let damage_rects = damage.as_ref().map(|d| d.len()).unwrap_or(0); + tracing::debug!( + width = output_size.w, + height = output_size.h, + bytes = pixels.len(), + damage_rects, + "framebuffer captured" + ); + } + Err(err) => { + tracing::warn!(?err, "failed to map framebuffer"); + } + }, + Err(err) => { + tracing::warn!(?err, "failed to copy framebuffer"); + } + } + + Ok(damage) + } + Err(err) => { + warn!(?err, "damage tracker render failed"); + Err(()) + } + } + }; + + if let Ok(damage) = render_result { + let has_damage = damage.is_some(); + + let submit_result = if let Some(ref rects) = damage { + backend.submit(Some(rects)) + } else { + backend.submit(None) + }; + + if let Err(err) = submit_result { + warn!(?err, "failed to submit frame"); + return; + } + + // Send frame callbacks to all mapped surfaces so clients + // know they can draw the next frame. + if has_damage { + let time = state.clock.now(); + for window in state.space.elements() { + window.send_frame(&output, time, Some(Duration::ZERO), |_, _| { + Some(output.clone()) + }); + } + } + } +} diff --git a/crates/wrsrvd/src/main.rs b/crates/wrsrvd/src/main.rs index 1ac10cf..fa3be20 100644 --- a/crates/wrsrvd/src/main.rs +++ b/crates/wrsrvd/src/main.rs @@ -1,35 +1,17 @@ +mod backend; mod errors; mod handlers; -mod render; mod state; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::Duration; - -use crate::handlers::ClientState; use crate::state::WayRay; use miette::Result; use smithay::{ - backend::{ - renderer::{damage::OutputDamageTracker, gles::GlesRenderer}, - winit::{self, WinitEvent, WinitGraphicsBackend}, - }, output::{Mode, Output, PhysicalProperties, Subpixel}, reexports::wayland_server::Display, utils::Transform, - wayland::{compositor::CompositorClientState, socket::ListeningSocketSource}, }; use tracing::info; -/// Data accessible from calloop event callbacks. -struct CalloopData { - state: WayRay, - display: Display, - backend: WinitGraphicsBackend, - damage_tracker: OutputDamageTracker, -} - fn main() -> Result<()> { tracing_subscriber::fmt() .with_env_filter( @@ -40,20 +22,17 @@ fn main() -> Result<()> { info!("wrsrvd starting"); + // Parse backend selection from CLI args. + let args: Vec = std::env::args().collect(); + let use_winit = args + .windows(2) + .any(|w| w[0] == "--backend" && w[1] == "winit"); + // Create the Wayland display. let mut display = Display::::new().map_err(|e| errors::WayRayError::DisplayInit(Box::new(e)))?; - // Initialize the Winit backend (opens a window, creates a GlesRenderer). - let (backend, winit_event_loop) = winit::init::().map_err(|e| { - errors::WayRayError::BackendInit(Box::::from( - e.to_string(), - )) - })?; - info!("winit backend initialized"); - - // Create a virtual output matching the window size. - let window_size = backend.window_size(); + // Create a virtual output. let output = Output::new( "wayray-0".to_string(), PhysicalProperties { @@ -64,9 +43,11 @@ fn main() -> Result<()> { }, ); + // Default to 1280x720 for headless; Winit will use its window size + // but we still need an initial mode for state setup. let mode = Mode { - size: window_size, - refresh: 60_000, // 60 Hz in millihertz + size: (1280, 720).into(), + refresh: 60_000, }; output.change_current_state(Some(mode), Some(Transform::Normal), None, None); output.set_preferred(mode); @@ -75,110 +56,16 @@ fn main() -> Result<()> { output.create_global::(&display.handle()); // Create compositor state. - let mut state = WayRay::new(&mut display, output.clone()); + let state = WayRay::new(&mut display, output.clone()); - // Map the output into the compositor space. - state.space.map_output(&output, (0, 0)); - info!("output mapped: {:?} @ {:?}", mode.size, mode.refresh); + info!( + backend = if use_winit { "winit" } else { "headless" }, + "dispatching to backend" + ); - // Create a Wayland listening socket for clients. - let listening_socket = ListeningSocketSource::new_auto() - .map_err(|e| errors::WayRayError::DisplayInit(Box::new(e)))?; - let socket_name = listening_socket.socket_name().to_os_string(); - info!(?socket_name, "wayland socket created"); - - // Set WAYLAND_DISPLAY so child processes can find us. - // SAFETY: This is called early in main before any other threads are spawned, - // so there are no concurrent readers of the environment. - unsafe { std::env::set_var("WAYLAND_DISPLAY", &socket_name) }; - - // Create the calloop event loop. - let mut event_loop: smithay::reexports::calloop::EventLoop = - smithay::reexports::calloop::EventLoop::try_new() - .map_err(|e| errors::WayRayError::EventLoop(Box::new(e)))?; - - let loop_handle = event_loop.handle(); - - // Insert the Wayland listening socket as a calloop source. - // When a new client connects, insert it into the display. - loop_handle - .insert_source(listening_socket, |client_stream, _, data| { - data.display - .handle() - .insert_client( - client_stream, - Arc::new(ClientState { - compositor_state: CompositorClientState::default(), - }), - ) - .ok(); - }) - .map_err(|e| errors::WayRayError::EventLoop(Box::new(e.error)))?; - - // Shared flag to signal the main loop to exit. - let running = Arc::new(AtomicBool::new(true)); - let running_clone = running.clone(); - - // Insert the Winit event loop as a calloop source. - loop_handle - .insert_source(winit_event_loop, move |event, _, data| match event { - WinitEvent::Resized { size, scale_factor } => { - // TODO: Update output mode, damage tracker, and space mapping - // to reflect the new window size. Currently the compositor - // continues rendering at the original size after resize. - info!(?size, scale_factor, "window resized"); - } - WinitEvent::Focus(focused) => { - info!(focused, "window focus changed"); - } - WinitEvent::Input(event) => { - data.state.process_input_event(event); - } - WinitEvent::Redraw => { - render::render_output_frame( - &mut data.state, - &mut data.backend, - &mut data.damage_tracker, - ); - } - WinitEvent::CloseRequested => { - info!("close requested, shutting down"); - running_clone.store(false, Ordering::SeqCst); - } - }) - .map_err(|e| errors::WayRayError::EventLoop(Box::new(e.error)))?; - - // Create a damage tracker for efficient rendering. - let damage_tracker = OutputDamageTracker::from_output(&output); - - let mut calloop_data = CalloopData { - state, - display, - backend, - damage_tracker, - }; - - info!("entering main event loop"); - - // Main event loop. - while running.load(Ordering::SeqCst) { - // Dispatch Wayland clients. - calloop_data - .display - .dispatch_clients(&mut calloop_data.state) - .map_err(|e| errors::WayRayError::EventLoop(Box::new(e)))?; - - calloop_data - .display - .flush_clients() - .map_err(|e| errors::WayRayError::EventLoop(Box::new(e)))?; - - // Dispatch calloop sources (Winit events + Wayland socket) with ~16ms timeout. - event_loop - .dispatch(Duration::from_millis(16), &mut calloop_data) - .map_err(|e| errors::WayRayError::EventLoop(Box::new(e)))?; + if use_winit { + backend::winit::run(display, state, output) + } else { + backend::headless::run(display, state, output) } - - info!("wrsrvd shutting down"); - Ok(()) } diff --git a/crates/wrsrvd/src/render.rs b/crates/wrsrvd/src/render.rs deleted file mode 100644 index 430393d..0000000 --- a/crates/wrsrvd/src/render.rs +++ /dev/null @@ -1,132 +0,0 @@ -use smithay::{ - backend::{ - allocator::Fourcc, - renderer::{ - ExportMem, - damage::OutputDamageTracker, - element::texture::TextureRenderElement, - gles::{GlesRenderer, GlesTexture}, - }, - winit::WinitGraphicsBackend, - }, - desktop::{Window, space::render_output}, - utils::{Buffer as BufferCoord, Rectangle, Size}, -}; -use tracing::warn; - -use crate::state::WayRay; - -/// Dark grey clear color for the compositor background. -const CLEAR_COLOR: [f32; 4] = [0.1, 0.1, 0.1, 1.0]; - -/// Render the compositor space to the Winit backend window. -/// -/// Uses `OutputDamageTracker` for efficient re-rendering: only -/// damaged regions are redrawn each frame. -/// -/// Returns `true` if any damage was present and submitted. -pub fn render_output_frame( - state: &mut WayRay, - backend: &mut WinitGraphicsBackend, - damage_tracker: &mut OutputDamageTracker, -) -> bool { - let output = state.output.clone(); - - // Get buffer age before bind (avoids borrow conflict). - let age = backend.buffer_age().unwrap_or(0); - - // Render within a block so framebuffer is dropped before submit. - let render_result = { - let (renderer, mut framebuffer) = match backend.bind() { - Ok(pair) => pair, - Err(err) => { - warn!(?err, "failed to bind winit backend for rendering"); - return false; - } - }; - - // The empty custom elements slice needs a concrete type. - let custom_elements: &[TextureRenderElement] = &[]; - - let render_result = render_output::<_, _, Window, _>( - &output, - renderer, - &mut framebuffer, - 1.0, // alpha - age, - [&state.space], - custom_elements, - damage_tracker, - CLEAR_COLOR, - ); - - match render_result { - Ok(result) => { - let damage = result.damage.cloned(); - - // Verify framebuffer capture path works (will be consumed - // by network transport in Phase 1). - let output_size = state.output.current_mode().unwrap().size; - let region: Rectangle = - Rectangle::from_size(Size::from((output_size.w, output_size.h))); - - match renderer.copy_framebuffer(&framebuffer, region, Fourcc::Argb8888) { - Ok(mapping) => match renderer.map_texture(&mapping) { - Ok(pixels) => { - let damage_rects = damage.as_ref().map(|d| d.len()).unwrap_or(0); - tracing::debug!( - width = output_size.w, - height = output_size.h, - bytes = pixels.len(), - damage_rects, - "framebuffer captured" - ); - } - Err(err) => { - tracing::warn!(?err, "failed to map framebuffer"); - } - }, - Err(err) => { - tracing::warn!(?err, "failed to copy framebuffer"); - } - } - - Ok(damage) - } - Err(err) => { - warn!(?err, "damage tracker render failed"); - Err(()) - } - } - }; - // framebuffer is now dropped, backend is no longer borrowed. - - match render_result { - Ok(damage) => { - let has_damage = damage.is_some(); - - let submit_result = if let Some(ref rects) = damage { - backend.submit(Some(rects)) - } else { - backend.submit(None) - }; - - if let Err(err) = submit_result { - warn!(?err, "failed to submit frame"); - return false; - } - - // Send frame callbacks to all mapped surfaces so clients - // know they can draw the next frame. - let time = state.clock.now(); - for window in state.space.elements() { - window.send_frame(&output, time, Some(std::time::Duration::ZERO), |_, _| { - Some(output.clone()) - }); - } - - has_damage - } - Err(()) => false, - } -}