From 564c473ab4989798dc9b917a0c4122bc7009142a Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Tue, 7 Apr 2026 17:08:55 +0200 Subject: [PATCH] Wire end-to-end frame encoding and network into headless backend Start QUIC server in wrsrvd main.rs before dispatching to backend, passing NetworkHandle to the headless backend. The headless render loop now encodes each frame as XOR diff against the previous frame, compresses damage regions with zstd, and sends FrameUpdate messages to connected clients via the network channel. Network input from remote clients is drained each iteration of the main event loop and injected into the compositor via inject_network_input(). Connection state (client connected/disconnected) is tracked to avoid encoding frames when no client is listening. --- crates/wrsrvd/src/backend/headless.rs | 153 ++++++++++++++++++++++++-- crates/wrsrvd/src/main.rs | 16 ++- 2 files changed, 158 insertions(+), 11 deletions(-) diff --git a/crates/wrsrvd/src/backend/headless.rs b/crates/wrsrvd/src/backend/headless.rs index 5b7074e..c70c732 100644 --- a/crates/wrsrvd/src/backend/headless.rs +++ b/crates/wrsrvd/src/backend/headless.rs @@ -24,9 +24,12 @@ use smithay::{ wayland::{compositor::CompositorClientState, socket::ListeningSocketSource}, }; use tracing::{info, warn}; +use wayray_protocol::encoding; +use wayray_protocol::messages::FrameUpdate; use crate::errors::WayRayError; use crate::handlers::ClientState; +use crate::network::{CompositorToNet, NetToCompositor, NetworkHandle}; use crate::state::WayRay; /// Dark grey clear color for the compositor background. @@ -39,13 +42,26 @@ struct CalloopData { renderer: PixmanRenderer, render_buffer: pixman_lib::Image<'static, 'static>, damage_tracker: OutputDamageTracker, + /// Network handle for sending frames and receiving input. + net_handle: NetworkHandle, + /// Previous frame's pixel data for XOR diff encoding. + previous_frame: Vec, + /// Frame sequence counter. + frame_sequence: u64, + /// Whether a remote client is currently connected. + client_connected: bool, } /// 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<()> { +pub fn run( + display: Display, + mut state: WayRay, + output: Output, + net_handle: NetworkHandle, +) -> Result<()> { // Create the PixmanRenderer (CPU software renderer). let mut renderer = PixmanRenderer::new().map_err(|e| { WayRayError::BackendInit(Box::::from( @@ -117,12 +133,20 @@ pub fn run(display: Display, mut state: WayRay, output: Output) -> Resul // Map the output into the compositor space. state.space.map_output(&output, (0, 0)); + // Initialize previous frame buffer (all zeros = black). + let frame_size = (output_size.w * output_size.h * 4) as usize; + let previous_frame = vec![0u8; frame_size]; + let mut calloop_data = CalloopData { state, display, renderer, render_buffer, damage_tracker, + net_handle, + previous_frame, + frame_sequence: 0, + client_connected: false, }; let running = Arc::new(AtomicBool::new(true)); @@ -134,6 +158,9 @@ pub fn run(display: Display, mut state: WayRay, output: Output) -> Resul info!("entering headless main event loop"); while running.load(Ordering::SeqCst) { + // Drain network events (input from remote clients, connection state). + drain_network_events(&mut calloop_data); + // Dispatch Wayland clients. calloop_data .display @@ -152,9 +179,44 @@ pub fn run(display: Display, mut state: WayRay, output: Output) -> Resul } info!("headless backend shutting down"); + calloop_data.net_handle.shutdown(); Ok(()) } +/// Drain all pending network events (input, connection changes). +fn drain_network_events(data: &mut CalloopData) { + use std::sync::mpsc::TryRecvError; + + loop { + match data.net_handle.rx.try_recv() { + Ok(NetToCompositor::Input(input_msg)) => { + data.state.inject_network_input(input_msg); + } + Ok(NetToCompositor::ClientConnected(hello)) => { + info!( + version = hello.version, + capabilities = ?hello.capabilities, + "remote client connected" + ); + data.client_connected = true; + } + Ok(NetToCompositor::ClientDisconnected) => { + info!("remote client disconnected"); + data.client_connected = false; + } + Ok(NetToCompositor::Control(ctrl)) => { + tracing::debug!(?ctrl, "received control message"); + } + Err(TryRecvError::Empty) => break, + Err(TryRecvError::Disconnected) => { + warn!("network channel disconnected"); + data.client_connected = false; + break; + } + } + } +} + /// Render a single frame using the headless PixmanRenderer. fn render_headless_frame(data: &mut CalloopData) { let output = data.state.output.clone(); @@ -197,14 +259,16 @@ fn render_headless_frame(data: &mut CalloopData) { { 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" - ); + // Send frame over network if a client is connected. + if data.client_connected { + send_frame_to_network( + data, + pixels, + &damage, + output_size.w, + output_size.h, + ); + } } Err(err) => { tracing::warn!(?err, "failed to map headless framebuffer"); @@ -229,6 +293,77 @@ fn render_headless_frame(data: &mut CalloopData) { } } +/// Encode the current frame as XOR diff against the previous frame and +/// send it to the connected client via the network channel. +fn send_frame_to_network( + data: &mut CalloopData, + current_pixels: &[u8], + damage: &Option>>, + width: i32, + height: i32, +) { + let stride = width as usize * 4; + + // Compute XOR diff. + let diff = encoding::xor_diff(current_pixels, &data.previous_frame); + + // Encode damaged regions. If damage tracking reports specific rects, use those; + // otherwise encode the full frame as a single region. + let regions: Vec<_> = match damage { + Some(rects) if !rects.is_empty() => rects + .iter() + .map(|rect| { + encoding::encode_region( + &diff, + stride, + rect.loc.x.max(0) as u32, + rect.loc.y.max(0) as u32, + (rect.size.w.min(width - rect.loc.x.max(0))).max(0) as u32, + (rect.size.h.min(height - rect.loc.y.max(0))).max(0) as u32, + ) + }) + .filter(|r| r.width > 0 && r.height > 0) + .collect(), + _ => { + // Full-frame update. + vec![encoding::encode_region( + &diff, + stride, + 0, + 0, + width as u32, + height as u32, + )] + } + }; + + data.frame_sequence += 1; + let frame_update = FrameUpdate { + sequence: data.frame_sequence, + regions, + }; + + tracing::debug!( + sequence = data.frame_sequence, + regions = frame_update.regions.len(), + "sending frame to client" + ); + + if data + .net_handle + .tx + .send(CompositorToNet::SendFrame(frame_update)) + .is_err() + { + warn!("failed to send frame to network thread"); + data.client_connected = false; + } + + // Store current frame as previous for next diff. + data.previous_frame.clear(); + data.previous_frame.extend_from_slice(current_pixels); +} + /// Install a Ctrl-C handler that sets the running flag to false. fn ctrlc_handler(running: &Arc) { let r = running.clone(); diff --git a/crates/wrsrvd/src/main.rs b/crates/wrsrvd/src/main.rs index 6da893e..63ac49f 100644 --- a/crates/wrsrvd/src/main.rs +++ b/crates/wrsrvd/src/main.rs @@ -4,6 +4,7 @@ mod handlers; pub mod network; mod state; +use crate::network::{ServerConfig, start_server}; use crate::state::WayRay; use miette::Result; use smithay::{ @@ -59,14 +60,25 @@ fn main() -> Result<()> { // Create compositor state. let state = WayRay::new(&mut display, output.clone()); + // Start the QUIC network server for remote client connections. + let output_size = mode.size; + let net_handle = start_server(ServerConfig { + output_width: output_size.w as u32, + output_height: output_size.h as u32, + ..ServerConfig::default() + }); + info!("QUIC network server started"); + info!( backend = if use_winit { "winit" } else { "headless" }, "dispatching to backend" ); if use_winit { - backend::winit::run(display, state, output) + let result = backend::winit::run(display, state, output); + net_handle.shutdown(); + result } else { - backend::headless::run(display, state, output) + backend::headless::run(display, state, output, net_handle) } }