Add rendering pipeline for client surfaces via OutputDamageTracker

- Create render module using Smithay's desktop::space::render_output
  for damage-tracked frame rendering to the Winit backend window
- Store backend and damage_tracker in CalloopData so the Winit event
  callback can trigger rendering on Redraw events
- Send initial xdg toplevel configure on first commit so clients can
  begin drawing
- Send frame callbacks after each render so clients schedule redraws
This commit is contained in:
Till Wegmueller 2026-04-04 18:45:31 +02:00
parent 50c8f68906
commit cbfc6e95df
3 changed files with 138 additions and 7 deletions

View file

@ -7,6 +7,7 @@ use smithay::{
wayland::{
buffer::BufferHandler,
compositor::{CompositorClientState, CompositorHandler, CompositorState},
seat::WaylandFocus,
shm::{ShmHandler, ShmState},
},
};
@ -44,6 +45,22 @@ impl CompositorHandler for WayRay {
fn commit(&mut self, surface: &WlSurface) {
trace!(?surface, "surface commit");
smithay::backend::renderer::utils::on_commit_buffer_handler::<Self>(surface);
// If this surface belongs to an xdg toplevel that hasn't received
// its initial configure yet, send it now so the client can start
// drawing.
if let Some(window) = self
.space
.elements()
.find(|w| w.wl_surface().map(|s| s.into_owned()) == Some(surface.clone()))
.cloned()
{
if let Some(toplevel) = window.toplevel() {
if !toplevel.is_initial_configure_sent() {
toplevel.send_configure();
}
}
}
}
}

View file

@ -1,5 +1,6 @@
mod errors;
mod handlers;
mod render;
mod state;
use std::sync::Arc;
@ -11,8 +12,8 @@ use crate::state::WayRay;
use miette::Result;
use smithay::{
backend::{
renderer::gles::GlesRenderer,
winit::{self, WinitEvent},
renderer::{damage::OutputDamageTracker, gles::GlesRenderer},
winit::{self, WinitEvent, WinitGraphicsBackend},
},
output::{Mode, Output, PhysicalProperties, Subpixel},
reexports::wayland_server::Display,
@ -25,6 +26,8 @@ use tracing::info;
struct CalloopData {
state: WayRay,
display: Display<WayRay>,
backend: WinitGraphicsBackend<GlesRenderer>,
damage_tracker: OutputDamageTracker,
}
fn main() -> Result<()> {
@ -118,7 +121,7 @@ fn main() -> Result<()> {
// Insert the Winit event loop as a calloop source.
loop_handle
.insert_source(winit_event_loop, move |event, _, _data| match event {
.insert_source(winit_event_loop, move |event, _, data| match event {
WinitEvent::Resized { size, scale_factor } => {
info!(?size, scale_factor, "window resized");
}
@ -129,7 +132,11 @@ fn main() -> Result<()> {
// Input handling is Task 7 -- ignore for now.
}
WinitEvent::Redraw => {
// Rendering is Task 6 -- ignore for now.
render::render_output_frame(
&mut data.state,
&mut data.backend,
&mut data.damage_tracker,
);
}
WinitEvent::CloseRequested => {
info!("close requested, shutting down");
@ -138,10 +145,15 @@ fn main() -> Result<()> {
})
.map_err(|e| errors::WayRayError::EventLoop(Box::new(e.error)))?;
// Keep a handle to the backend (needed for rendering in Task 6).
let _backend = backend;
// Create a damage tracker for efficient rendering.
let damage_tracker = OutputDamageTracker::from_output(&output);
let mut calloop_data = CalloopData { state, display };
let mut calloop_data = CalloopData {
state,
display,
backend,
damage_tracker,
};
info!("entering main event loop");

102
crates/wrsrvd/src/render.rs Normal file
View file

@ -0,0 +1,102 @@
use smithay::{
backend::{
renderer::{
damage::OutputDamageTracker,
element::texture::TextureRenderElement,
gles::{GlesRenderer, GlesTexture},
},
winit::WinitGraphicsBackend,
},
desktop::{Window, space::render_output},
};
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_damage = {
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) => {
// Clone the damage rectangles so we can use them after
// the framebuffer is dropped.
Ok(result.damage.cloned())
}
Err(err) => {
warn!(?err, "damage tracker render failed");
Err(())
}
}
};
// framebuffer is now dropped, backend is no longer borrowed.
match render_damage {
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,
}
}