Add headless backend with PixmanRenderer, refactor into backend modules

Restructure wrsrvd to support two backends: a headless PixmanRenderer
(default) for running without a display server, and the existing Winit
backend (via --backend winit). The render logic is split into per-backend
modules, and the old render.rs is removed.
This commit is contained in:
Till Wegmueller 2026-04-07 16:29:32 +02:00
parent f394d8cd7d
commit 8a3d14ff19
7 changed files with 633 additions and 296 deletions

139
Cargo.lock generated
View file

@ -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",

View file

@ -15,5 +15,7 @@ smithay = { version = "0.7", default-features = false, features = [
"wayland_frontend",
"desktop",
"renderer_gl",
"renderer_pixman",
"backend_winit",
] }
ctrlc = "3"

View file

@ -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<WayRay>,
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<WayRay>, mut state: WayRay, output: Output) -> Result<()> {
// Create the PixmanRenderer (CPU software renderer).
let mut renderer = PixmanRenderer::new().map_err(|e| {
WayRayError::BackendInit(Box::<dyn std::error::Error + Send + Sync>::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<i32, BufferCoord> = 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::<dyn std::error::Error + Send + Sync>::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<CalloopData> =
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<PixmanTexture>] = &[];
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<i32, BufferCoord> =
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<AtomicBool>) {
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);
});
}

View file

@ -0,0 +1,2 @@
pub mod headless;
pub mod winit;

View file

@ -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<WayRay>,
backend: WinitGraphicsBackend<GlesRenderer>,
damage_tracker: OutputDamageTracker,
}
/// Run the compositor with the Winit backend (opens a window for development).
pub fn run(display: Display<WayRay>, mut state: WayRay, output: Output) -> Result<()> {
// Initialize the Winit backend (opens a window, creates a GlesRenderer).
let (backend, winit_event_loop) = winit::init::<GlesRenderer>().map_err(|e| {
WayRayError::BackendInit(Box::<dyn std::error::Error + Send + Sync>::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<CalloopData> =
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<GlesRenderer>,
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<GlesTexture>] = &[];
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<i32, BufferCoord> =
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())
});
}
}
}
}

View file

@ -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<WayRay>,
backend: WinitGraphicsBackend<GlesRenderer>,
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<String> = 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::<WayRay>::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::<GlesRenderer>().map_err(|e| {
errors::WayRayError::BackendInit(Box::<dyn std::error::Error + Send + Sync>::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::<WayRay>(&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<CalloopData> =
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(())
}

View file

@ -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<GlesRenderer>,
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<GlesTexture>] = &[];
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<i32, BufferCoord> =
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,
}
}