diff --git a/docs/ai/plans/002-phase0-foundation.md b/docs/ai/plans/002-phase0-foundation.md new file mode 100644 index 0000000..537a610 --- /dev/null +++ b/docs/ai/plans/002-phase0-foundation.md @@ -0,0 +1,1292 @@ +# Phase 0: Foundation — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Set up the Cargo workspace, build a minimal Smithay compositor that hosts Wayland clients, and capture framebuffers with damage tracking. + +**Architecture:** Four-crate Cargo workspace (`wrsrvd`, `wrclient`, `wayray-protocol`, `wradm`) under `crates/`. Smithay with `default-features = false` and only portable features. Winit backend for dev/testing. PixmanRenderer is the design default but Phase 0 uses GlesRenderer via Winit for visual feedback. Headless/Pixman path deferred to Phase 1. + +**Tech Stack:** Rust 2024, Smithay (wayland_frontend, xwayland, desktop, renderer_gl, backend_winit), calloop, tracing, miette, wayland-server + +**Spec:** `docs/ai/plans/001-implementation-roadmap.md` (Phase 0), `docs/ai/adr/001-compositor-framework.md`, `docs/ai/adr/005-rendering-strategy.md`, `docs/ai/adr/007-project-structure.md`, `docs/ai/adr/008-illumos-support.md` + +**Milestones:** +- **M0a:** Workspace builds, all crates compile (empty) +- **M0b:** Compositor runs, opens a Winit window, Wayland clients can connect +- **M0c:** Compositor renders client surfaces in the window +- **M0d:** Framebuffer capture works, damage regions tracked + +--- + +## File Structure + +``` +wayray/ +├── Cargo.toml # Workspace root +├── crates/ +│ ├── wayray-protocol/ +│ │ ├── Cargo.toml +│ │ └── src/ +│ │ └── lib.rs # Shared types (empty for Phase 0, placeholder) +│ ├── wrsrvd/ +│ │ ├── Cargo.toml +│ │ └── src/ +│ │ ├── main.rs # Entry point: arg parsing, tracing init, event loop +│ │ ├── state.rs # WayRay compositor state struct + all Smithay handler impls +│ │ ├── handlers/ +│ │ │ ├── mod.rs # Re-export handler modules +│ │ │ ├── compositor.rs # CompositorHandler, commit logic +│ │ │ ├── xdg_shell.rs # XdgShellHandler, toplevel/popup lifecycle +│ │ │ └── input.rs # SeatHandler, keyboard/pointer focus +│ │ └── render.rs # Render loop: render elements, damage tracking, framebuffer capture +│ ├── wrclient/ +│ │ ├── Cargo.toml +│ │ └── src/ +│ │ └── main.rs # Placeholder binary +│ └── wradm/ +│ ├── Cargo.toml +│ └── src/ +│ └── main.rs # Placeholder binary +└── tests/ # Integration tests (future) +``` + +--- + +## Task 1: Workspace Scaffold + +**Files:** +- Rewrite: `Cargo.toml` (workspace root) +- Create: `crates/wayray-protocol/Cargo.toml` +- Create: `crates/wayray-protocol/src/lib.rs` +- Create: `crates/wrsrvd/Cargo.toml` +- Create: `crates/wrsrvd/src/main.rs` +- Create: `crates/wrclient/Cargo.toml` +- Create: `crates/wrclient/src/main.rs` +- Create: `crates/wradm/Cargo.toml` +- Create: `crates/wradm/src/main.rs` +- Delete: `src/lib.rs` (default crate code) + +- [ ] **Step 1: Create workspace root Cargo.toml** + +Replace the existing `Cargo.toml` with a workspace definition. No `[package]` at root — workspace only. + +```toml +[workspace] +resolver = "3" +members = [ + "crates/wayray-protocol", + "crates/wrsrvd", + "crates/wrclient", + "crates/wradm", +] + +[workspace.package] +edition = "2024" +version = "0.1.0" +license = "MPL-2.0" + +[workspace.dependencies] +wayray-protocol = { path = "crates/wayray-protocol" } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +miette = { version = "7", features = ["fancy"] } +thiserror = "2" +``` + +- [ ] **Step 2: Create wayray-protocol crate** + +`crates/wayray-protocol/Cargo.toml`: +```toml +[package] +name = "wayray-protocol" +edition.workspace = true +version.workspace = true +license.workspace = true + +[dependencies] +``` + +`crates/wayray-protocol/src/lib.rs`: +```rust +//! WayRay wire protocol definitions. +//! +//! Shared between wrsrvd (server) and wrclient (client). +``` + +- [ ] **Step 3: Create wrsrvd crate** + +`crates/wrsrvd/Cargo.toml`: +```toml +[package] +name = "wrsrvd" +edition.workspace = true +version.workspace = true +license.workspace = true + +[dependencies] +wayray-protocol.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +miette.workspace = true +thiserror.workspace = true + +smithay = { version = "0.3", default-features = false, features = [ + "wayland_frontend", + "desktop", + "renderer_gl", + "backend_winit", +] } +``` + +`crates/wrsrvd/src/main.rs`: +```rust +fn main() { + println!("wrsrvd compositor"); +} +``` + +- [ ] **Step 4: Create wrclient and wradm placeholder crates** + +`crates/wrclient/Cargo.toml`: +```toml +[package] +name = "wrclient" +edition.workspace = true +version.workspace = true +license.workspace = true + +[dependencies] +wayray-protocol.workspace = true +tracing.workspace = true +miette.workspace = true +``` + +`crates/wrclient/src/main.rs`: +```rust +fn main() { + println!("wrclient viewer"); +} +``` + +`crates/wradm/Cargo.toml`: +```toml +[package] +name = "wradm" +edition.workspace = true +version.workspace = true +license.workspace = true + +[dependencies] +wayray-protocol.workspace = true +tracing.workspace = true +miette.workspace = true +``` + +`crates/wradm/src/main.rs`: +```rust +fn main() { + println!("wradm administration tool"); +} +``` + +- [ ] **Step 5: Remove old src/lib.rs** + +```bash +rm src/lib.rs +rmdir src +``` + +- [ ] **Step 6: Verify workspace builds** + +```bash +cargo build --workspace +``` + +Expected: All four crates compile. Smithay downloads and builds with the specified features. + +- [ ] **Step 7: Run each binary to verify** + +```bash +cargo run --bin wrsrvd +cargo run --bin wrclient +cargo run --bin wradm +``` + +Expected: Each prints its name and exits. + +- [ ] **Step 8: Commit** + +```bash +git add -A +git commit -m "Set up Cargo workspace with four crates + +Workspace: wrsrvd, wrclient, wayray-protocol, wradm under crates/. +Smithay configured with default-features=false, portable features only. +Implements ADR-007 project structure." +``` + +--- + +## Task 2: Tracing and Error Infrastructure + +**Files:** +- Modify: `crates/wrsrvd/src/main.rs` +- Create: `crates/wrsrvd/src/errors.rs` + +- [ ] **Step 1: Create error types** + +`crates/wrsrvd/src/errors.rs`: +```rust +use miette::Diagnostic; +use thiserror::Error; + +#[derive(Debug, Error, Diagnostic)] +pub enum WayRayError { + #[error("failed to initialize Winit backend")] + #[diagnostic(help("ensure a display server (X11 or Wayland) is running"))] + BackendInit(#[source] Box), + + #[error("failed to initialize Wayland display")] + #[diagnostic(help("check that the XDG_RUNTIME_DIR environment variable is set"))] + DisplayInit(#[source] Box), + + #[error("event loop error")] + EventLoop(#[source] Box), +} +``` + +- [ ] **Step 2: Set up tracing and miette in main.rs** + +`crates/wrsrvd/src/main.rs`: +```rust +mod errors; + +use miette::{IntoDiagnostic, Result}; +use tracing::info; + +fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + info!("wrsrvd starting"); + Ok(()) +} +``` + +- [ ] **Step 3: Verify it runs with RUST_LOG** + +```bash +RUST_LOG=debug cargo run --bin wrsrvd +``` + +Expected: Tracing output with "wrsrvd starting" at info level. Debug level shows more output. + +- [ ] **Step 4: Commit** + +```bash +git add crates/wrsrvd/src/errors.rs crates/wrsrvd/src/main.rs +git commit -m "Add tracing and miette error infrastructure to wrsrvd" +``` + +--- + +## Task 3: Compositor State Struct + +**Files:** +- Create: `crates/wrsrvd/src/state.rs` +- Modify: `crates/wrsrvd/src/main.rs` + +This task defines the central `WayRay` state struct that holds all Smithay subsystem states. No handler impls yet — just the struct and construction. + +- [ ] **Step 1: Create the state struct** + +`crates/wrsrvd/src/state.rs`: +```rust +use smithay::{ + desktop::Space, + input::{SeatState, Seat}, + reexports::wayland_server::{Display, DisplayHandle}, + utils::Clock, + wayland::{ + compositor::CompositorState, + shell::xdg::XdgShellState, + shm::ShmState, + output::OutputManagerState, + }, +}; + +pub struct WayRay { + pub display_handle: DisplayHandle, + pub compositor_state: CompositorState, + pub xdg_shell_state: XdgShellState, + pub shm_state: ShmState, + pub seat_state: SeatState, + pub output_manager_state: OutputManagerState, + pub space: Space, + pub seat: Seat, + pub clock: Clock, +} + +impl WayRay { + pub fn new(display: &mut Display) -> Self { + let dh = display.handle(); + let clock = Clock::new(); + + let compositor_state = CompositorState::new::(&dh); + let xdg_shell_state = XdgShellState::new::(&dh); + let shm_state = ShmState::new::(&dh, vec![]); + let mut seat_state = SeatState::new(); + let output_manager_state = OutputManagerState::new_with_xdg_output::(&dh); + let space = Space::default(); + + let seat = seat_state.new_wl_seat(&dh, "wayray"); + + Self { + display_handle: dh, + compositor_state, + xdg_shell_state, + shm_state, + seat_state, + output_manager_state, + space, + seat, + clock, + } + } +} +``` + +- [ ] **Step 2: Wire state into main.rs** + +Update `crates/wrsrvd/src/main.rs`: +```rust +mod errors; +mod state; + +use miette::{IntoDiagnostic, Result}; +use smithay::reexports::wayland_server::Display; +use tracing::info; + +use crate::state::WayRay; + +fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + info!("wrsrvd starting"); + + let mut display = Display::::new() + .map_err(|e| errors::WayRayError::DisplayInit(Box::new(e)))?; + + let _state = WayRay::new(&mut display); + + info!("compositor state initialized"); + Ok(()) +} +``` + +- [ ] **Step 3: Verify it compiles** + +```bash +cargo build --bin wrsrvd +``` + +Expected: Compiles. May require adjusting imports based on exact Smithay API version. Fix any compile errors — the Smithay API docs are at https://docs.rs/smithay/latest/smithay/. + +- [ ] **Step 4: Commit** + +```bash +git add crates/wrsrvd/src/state.rs crates/wrsrvd/src/main.rs +git commit -m "Define WayRay compositor state struct with Smithay subsystems" +``` + +--- + +## Task 4: Wayland Protocol Handlers + +**Files:** +- Create: `crates/wrsrvd/src/handlers/mod.rs` +- Create: `crates/wrsrvd/src/handlers/compositor.rs` +- Create: `crates/wrsrvd/src/handlers/xdg_shell.rs` +- Create: `crates/wrsrvd/src/handlers/input.rs` +- Modify: `crates/wrsrvd/src/state.rs` (add ClientData impl) +- Modify: `crates/wrsrvd/src/main.rs` (add mod handlers) + +These handlers are required for Wayland clients to connect and interact with the compositor. Each handler implements a Smithay trait and uses the `delegate_*!` macro. + +- [ ] **Step 1: Create ClientData and compositor handler** + +`crates/wrsrvd/src/handlers/compositor.rs`: +```rust +use smithay::{ + delegate_compositor, + wayland::compositor::{CompositorClientState, CompositorHandler, CompositorState}, + reexports::wayland_server::{ + Client, + protocol::wl_surface::WlSurface, + }, +}; +use tracing::trace; + +use crate::state::WayRay; + +/// Per-client state stored by wayland-server. +#[derive(Default)] +pub struct ClientState { + pub compositor_state: CompositorClientState, +} + +impl wayland_server::backend::ClientData for ClientState { + fn initialized(&self, _client_id: wayland_server::backend::ClientId) {} + fn disconnected( + &self, + _client_id: wayland_server::backend::ClientId, + _reason: wayland_server::backend::DisconnectReason, + ) {} +} + +impl CompositorHandler for WayRay { + fn compositor_state(&mut self) -> &mut CompositorState { + &mut self.compositor_state + } + + fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { + &client.get_data::().unwrap().compositor_state + } + + fn commit(&mut self, surface: &WlSurface) { + trace!(?surface, "surface commit"); + // Ensure surface state is applied to any mapped xdg toplevels + smithay::desktop::utils::on_commit_buffer_handler::(surface); + } +} + +delegate_compositor!(WayRay); +``` + +- [ ] **Step 2: Create xdg_shell handler** + +`crates/wrsrvd/src/handlers/xdg_shell.rs`: +```rust +use smithay::{ + delegate_xdg_shell, + desktop::{Space, Window}, + reexports::wayland_server::protocol::{wl_seat, wl_surface::WlSurface}, + utils::Serial, + wayland::shell::xdg::{ + PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState, + }, +}; +use tracing::info; + +use crate::state::WayRay; + +impl XdgShellHandler for WayRay { + fn xdg_shell_state(&mut self) -> &mut XdgShellState { + &mut self.xdg_shell_state + } + + fn new_toplevel(&mut self, surface: ToplevelSurface) { + let window = Window::new_wayland_window(surface); + self.space.map_element(window, (0, 0), false); + info!("new toplevel mapped"); + } + + fn new_popup(&mut self, _surface: PopupSurface, _positioner: PositionerState) { + // Popups handled minimally for now + } + + fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) { + // Popup grabs not implemented yet + } + + fn reposition_request( + &mut self, + _surface: PopupSurface, + _positioner: PositionerState, + _token: u32, + ) { + // Popup repositioning not implemented yet + } +} + +delegate_xdg_shell!(WayRay); +``` + +- [ ] **Step 3: Create input (seat) handler** + +`crates/wrsrvd/src/handlers/input.rs`: +```rust +use smithay::{ + delegate_seat, delegate_data_device, + input::{Seat, SeatHandler, SeatState, pointer::CursorImageStatus}, + reexports::wayland_server::protocol::wl_surface::WlSurface, + wayland::selection::data_device::{ + DataDeviceHandler, DataDeviceState, ClientDndGrabHandler, ServerDndGrabHandler, + set_data_device_focus, + }, +}; +use tracing::trace; + +use crate::state::WayRay; + +impl SeatHandler for WayRay { + type KeyboardFocus = WlSurface; + type PointerFocus = WlSurface; + type TouchFocus = WlSurface; + + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn focus_changed(&mut self, _seat: &Seat, _focused: Option<&WlSurface>) { + trace!("focus changed"); + } + + fn cursor_image(&mut self, _seat: &Seat, _image: CursorImageStatus) { + // Cursor image handling deferred to render task + } +} + +delegate_seat!(WayRay); + +// Data device (clipboard/drag-and-drop) — required by many Wayland clients. +impl DataDeviceHandler for WayRay { + fn data_device_state(&self) -> &DataDeviceState { + &self.data_device_state + } +} + +impl ClientDndGrabHandler for WayRay {} +impl ServerDndGrabHandler for WayRay {} + +delegate_data_device!(WayRay); +``` + +- [ ] **Step 4: Create handlers mod.rs** + +`crates/wrsrvd/src/handlers/mod.rs`: +```rust +mod compositor; +mod xdg_shell; +mod input; + +pub use compositor::ClientState; +``` + +- [ ] **Step 5: Add DataDeviceState to WayRay struct** + +Update `crates/wrsrvd/src/state.rs` to add: +- `data_device_state: DataDeviceState` field +- Import `smithay::wayland::selection::data_device::DataDeviceState` +- Initialize with `DataDeviceState::new::(&dh)` in `WayRay::new` + +- [ ] **Step 6: Add `mod handlers` to main.rs** + +Add `mod handlers;` to `crates/wrsrvd/src/main.rs`. + +- [ ] **Step 7: Verify it compiles** + +```bash +cargo build --bin wrsrvd +``` + +Expected: Compiles cleanly. The delegate macros generate the Wayland dispatch glue. Fix any import/API mismatches by consulting https://docs.rs/smithay/latest/smithay/. + +Note: The exact Smithay API may require adjustments — trait method signatures, import paths, and delegate macro patterns can vary between Smithay versions. The Smallvil example in the Smithay repo (https://github.com/Smithay/smithay/tree/master/smallvil) is the authoritative reference for a minimal compositor. Consult it if you encounter API mismatches. + +- [ ] **Step 8: Commit** + +```bash +git add crates/wrsrvd/src/handlers/ crates/wrsrvd/src/state.rs crates/wrsrvd/src/main.rs +git commit -m "Implement Wayland protocol handlers for compositor, xdg_shell, seat" +``` + +--- + +## Task 5: Winit Backend and Event Loop + +**Files:** +- Modify: `crates/wrsrvd/src/main.rs` +- Modify: `crates/wrsrvd/src/state.rs` + +This task wires up the Winit backend (opens a window, creates a GlesRenderer) and the calloop event loop. After this task, wrsrvd opens a window and accepts Wayland client connections. + +- [ ] **Step 1: Add Winit backend initialization to main.rs** + +Replace the body of `main()` in `crates/wrsrvd/src/main.rs` with the full startup sequence: + +```rust +mod errors; +mod handlers; +mod state; + +use miette::{IntoDiagnostic, Result}; +use smithay::{ + backend::{ + renderer::gles::GlesRenderer, + winit::{self, WinitEventLoop, WinitGraphicsBackend}, + }, + output::{Mode, Output, PhysicalProperties, Subpixel}, + reexports::{ + calloop::{EventLoop, LoopSignal}, + wayland_server::Display, + }, + utils::Transform, +}; +use tracing::info; + +use crate::{handlers::ClientState, state::WayRay}; + +fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + info!("wrsrvd starting"); + + // Wayland display + let mut display = Display::::new() + .map_err(|e| errors::WayRayError::DisplayInit(Box::new(e)))?; + + // Winit backend (opens a window, gives us a GlesRenderer) + let (mut backend, mut winit_event_loop): (WinitGraphicsBackend, WinitEventLoop) = + winit::init() + .map_err(|e| errors::WayRayError::BackendInit(Box::new(e)))?; + + // Create a virtual output matching the window size + let output = Output::new( + "wayray-0".to_string(), + PhysicalProperties { + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "WayRay".to_string(), + model: "Virtual".to_string(), + }, + ); + + let mode = Mode { + size: backend.window_size(), + refresh: 60_000, + }; + output.change_current_state(Some(mode), Some(Transform::Flipped180), None, Some((0, 0).into())); + output.set_preferred(mode); + + // Compositor state + let mut state = WayRay::new(&mut display, output.clone()); + + // Map the output into the compositor's space + state.space.map_output(&output, (0, 0)); + + // calloop event loop + let mut event_loop: EventLoop = EventLoop::try_new() + .map_err(|e| errors::WayRayError::EventLoop(Box::new(e)))?; + let loop_signal = event_loop.get_signal(); + + // Insert the Wayland display as an event source + event_loop + .handle() + .insert_source( + smithay::reexports::calloop::generic::Generic::new( + display.backend().poll_fd().try_clone_to_owned().unwrap(), + calloop::Interest::READ, + calloop::Mode::Level, + ), + move |_, _, _state| { + // This is handled below via display.dispatch_clients + Ok(calloop::PostAction::Continue) + }, + ) + .map_err(|e| errors::WayRayError::EventLoop(Box::new(e)))?; + + // Print the Wayland socket name so clients can connect + let socket_name = display + .add_socket_auto() + .map_err(|e| errors::WayRayError::DisplayInit(Box::new(e)))?; + info!(?socket_name, "Wayland socket listening"); + std::env::set_var("WAYLAND_DISPLAY", socket_name.clone()); + + info!("entering event loop"); + loop { + // Process Winit events (window resize, close, input) + let status = winit_event_loop.dispatch_new_events(|_event| { + // Input events handled here in a later task + }); + + if let winit::WinitEvent::CloseRequested = status { + info!("window close requested, shutting down"); + break; + } + + // Dispatch Wayland clients + display.dispatch_clients(&mut state) + .map_err(|e| errors::WayRayError::EventLoop(Box::new(e)))?; + display.flush_clients() + .map_err(|e| errors::WayRayError::EventLoop(Box::new(e)))?; + + // Tick the event loop + event_loop + .dispatch(Some(std::time::Duration::from_millis(16)), &mut state) + .map_err(|e| errors::WayRayError::EventLoop(e.into()))?; + } + + Ok(()) +} +``` + +- [ ] **Step 2: Update WayRay::new to accept Output** + +Modify `crates/wrsrvd/src/state.rs` — change the `new` signature to accept an `Output` parameter and store it: + +```rust +pub output: Output, +``` + +And in `new()`: +```rust +pub fn new(display: &mut Display, output: Output) -> Self { + // ... existing init ... + Self { + // ... existing fields ... + output, + } +} +``` + +- [ ] **Step 3: Verify it runs and opens a window** + +```bash +cargo run --bin wrsrvd +``` + +Expected: A window opens. Log output shows the Wayland socket name. Closing the window exits cleanly. + +- [ ] **Step 4: Test client connection** + +In a separate terminal: +```bash +WAYLAND_DISPLAY= weston-info +``` + +Expected: `weston-info` connects and lists the compositor's globals (wl_compositor, xdg_wm_base, wl_shm, wl_seat, wl_output). It may error on missing protocols — that's fine for now. + +Note: The exact event loop wiring depends on the Smithay version. The Smallvil example is the canonical reference. The code above is a guide — expect to adjust based on compile errors and the actual Smithay API. + +- [ ] **Step 5: Commit** + +```bash +git add crates/wrsrvd/src/main.rs crates/wrsrvd/src/state.rs +git commit -m "Wire up Winit backend and calloop event loop + +Opens a window, creates Wayland socket, accepts client connections." +``` + +--- + +## Task 6: Rendering Client Surfaces + +**Files:** +- Create: `crates/wrsrvd/src/render.rs` +- Modify: `crates/wrsrvd/src/main.rs` (call render in the loop) +- Modify: `crates/wrsrvd/src/handlers/compositor.rs` (schedule re-render on commit) + +This task makes client surfaces actually visible in the Winit window. Uses Smithay's Space to collect render elements and OutputDamageTracker for efficient rendering. + +- [ ] **Step 1: Create render module** + +`crates/wrsrvd/src/render.rs`: +```rust +use smithay::{ + backend::renderer::{ + damage::OutputDamageTracker, + element::surface::WaylandSurfaceRenderElement, + gles::GlesRenderer, + }, + desktop::space::SpaceRenderElements, +}; + +use crate::state::WayRay; + +/// Render elements type alias for the compositor. +pub type WayRayRenderElements = SpaceRenderElements>; + +/// Render the compositor space to the backend. +/// +/// Returns `true` if the output was updated (damage was present). +pub fn render_output( + state: &mut WayRay, + backend: &mut smithay::backend::winit::WinitGraphicsBackend, + damage_tracker: &mut OutputDamageTracker, +) -> bool { + let output = &state.output; + let space = &mut state.space; + + // Collect render elements from the space + let render_elements: Vec = + space.render_elements_for_output(backend.renderer(), output, 1.0).unwrap(); + + // Render with damage tracking + let render_result = backend.bind().and_then(|_| { + damage_tracker.render_output( + backend.renderer(), + &backend.framebuffer(), + 0, // buffer age — Winit doesn't provide this reliably + &render_elements, + [0.1, 0.1, 0.1, 1.0], // dark grey background + ) + }); + + match render_result { + Ok(render_output_result) => { + let has_damage = !render_output_result.damage.is_empty(); + if has_damage { + backend.submit(Some(&render_output_result.damage)).ok(); + } + + // Send frame callbacks to all visible surfaces + space.elements().for_each(|window| { + window.send_frame( + output, + state.clock.now(), + Some(std::time::Duration::ZERO), + |_, _| Some(output.clone()), + ); + }); + + has_damage + } + Err(err) => { + tracing::warn!(?err, "render error"); + false + } + } +} +``` + +- [ ] **Step 2: Wire render into the main loop** + +In `crates/wrsrvd/src/main.rs`, add to the main loop (after Winit event dispatch, before calloop dispatch): + +```rust +// Render +render::render_output(&mut state, &mut backend, &mut damage_tracker); +``` + +Initialize `damage_tracker` before the loop: +```rust +use smithay::backend::renderer::damage::OutputDamageTracker; + +let mut damage_tracker = OutputDamageTracker::from_output(&output); +``` + +Add `mod render;` at the top. + +- [ ] **Step 3: Trigger re-render on surface commit** + +In `crates/wrsrvd/src/handlers/compositor.rs`, update the `commit` method to also handle xdg surface initial configure: + +```rust +fn commit(&mut self, surface: &WlSurface) { + trace!(?surface, "surface commit"); + smithay::desktop::utils::on_commit_buffer_handler::(surface); + + // If this is an xdg toplevel that hasn't been configured yet, send initial configure + if let Some(window) = self.space.elements().find(|w| { + w.wl_surface().as_ref() == Some(surface) + }).cloned() { + if let Some(toplevel) = window.toplevel() { + if !toplevel.is_initial_configure_sent() { + toplevel.send_configure(); + } + } + } +} +``` + +- [ ] **Step 4: Verify rendering works** + +```bash +cargo run --bin wrsrvd +``` + +In another terminal: +```bash +WAYLAND_DISPLAY= foot # or weston-terminal, or any Wayland client +``` + +Expected: The Wayland client window appears inside the Winit compositor window with a dark grey background. + +- [ ] **Step 5: Commit** + +```bash +git add crates/wrsrvd/src/render.rs crates/wrsrvd/src/main.rs crates/wrsrvd/src/handlers/compositor.rs +git commit -m "Render client surfaces in the Winit window + +Space collects render elements, OutputDamageTracker handles +efficient re-rendering. Clients appear in the compositor window." +``` + +--- + +## Task 7: Keyboard and Pointer Input + +**Files:** +- Modify: `crates/wrsrvd/src/main.rs` (handle Winit input events) +- Modify: `crates/wrsrvd/src/state.rs` (add keyboard init) + +Without input handling, clients can't receive keyboard or mouse events. This task routes Winit input events through the Smithay seat. + +- [ ] **Step 1: Initialize keyboard on the seat** + +In `crates/wrsrvd/src/state.rs`, inside `WayRay::new()`, after creating the seat: + +```rust +seat.add_keyboard(Default::default(), 200, 25) + .expect("failed to add keyboard to seat"); +seat.add_pointer(); +``` + +- [ ] **Step 2: Handle Winit input events** + +In `crates/wrsrvd/src/main.rs`, replace the Winit event dispatch callback: + +```rust +use smithay::backend::winit::WinitEvent; +use smithay::backend::input::Event; + +let status = winit_event_loop.dispatch_new_events(|event| { + match event { + WinitEvent::Input(input_event) => { + // Forward input to the compositor state + state.process_input_event(input_event); + } + WinitEvent::Resized { size, .. } => { + let mode = Mode { + size, + refresh: 60_000, + }; + output.change_current_state(Some(mode), None, None, None); + } + _ => {} + } +}); +``` + +- [ ] **Step 3: Implement process_input_event on WayRay** + +Add to `crates/wrsrvd/src/state.rs`: + +```rust +use smithay::{ + backend::input::{ + self, InputEvent, KeyboardKeyEvent, PointerMotionAbsoluteEvent, + PointerButtonEvent, PointerAxisEvent, InputBackend, + }, + input::{ + keyboard::FilterResult, + pointer::{AxisFrame, ButtonEvent, MotionEvent}, + }, +}; + +impl WayRay { + pub fn process_input_event(&mut self, event: input::InputEvent) { + match event { + InputEvent::Keyboard { event } => { + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + let time = event.time_msec(); + let key_code = event.key_code(); + let state = event.state(); + let keyboard = self.seat.get_keyboard().unwrap(); + + keyboard.input::<(), _>( + self, + key_code, + state, + serial, + time, + |_, _, _| FilterResult::Forward, + ); + } + InputEvent::PointerMotionAbsolute { event } => { + let output_geo = self.space.output_geometry(&self.output).unwrap(); + let pos = event.position_transformed(output_geo.size); + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + + let pointer = self.seat.get_pointer().unwrap(); + let under = self.space.element_under(pos).map(|(w, loc)| { + (w.wl_surface().unwrap().into(), loc) + }); + + pointer.motion( + self, + under, + &MotionEvent { + location: pos.to_f64(), + serial, + time: event.time_msec(), + }, + ); + } + InputEvent::PointerButton { event } => { + let pointer = self.seat.get_pointer().unwrap(); + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + + // Set keyboard focus to the window under the pointer on click + if event.state() == input::ButtonState::Pressed { + if let Some((window, _)) = self.space.element_under(pointer.current_location()) { + let window = window.clone(); + self.space.raise_element(&window, true); + let wl_surface = window.wl_surface().unwrap(); + self.seat.get_keyboard().unwrap().set_focus( + self, + Some(wl_surface), + serial, + ); + } + } + + pointer.button( + self, + &ButtonEvent { + button: event.button_code(), + state: event.state().into(), + serial, + time: event.time_msec(), + }, + ); + } + InputEvent::PointerAxis { event } => { + let pointer = self.seat.get_pointer().unwrap(); + let source = event.source(); + let mut frame = AxisFrame::new(event.time_msec()).source(source); + + use smithay::backend::input::Axis; + if let Some(amount) = event.amount(Axis::Horizontal) { + frame = frame.value(smithay::input::pointer::Axis::Horizontal, amount); + } + if let Some(amount) = event.amount(Axis::Vertical) { + frame = frame.value(smithay::input::pointer::Axis::Vertical, amount); + } + + pointer.axis(self, frame); + pointer.frame(self); + } + _ => {} + } + } +} +``` + +- [ ] **Step 4: Verify input works** + +```bash +cargo run --bin wrsrvd +``` + +Open a client: +```bash +WAYLAND_DISPLAY= foot +``` + +Expected: You can click on the terminal window to focus it. Typing produces text. Mouse events work. + +- [ ] **Step 5: Commit** + +```bash +git add crates/wrsrvd/src/state.rs crates/wrsrvd/src/main.rs +git commit -m "Route Winit input events through Smithay seat + +Keyboard, pointer motion, click-to-focus, and scroll wheel +forwarded to Wayland clients." +``` + +--- + +## Task 8: Framebuffer Capture with ExportMem + +**Files:** +- Modify: `crates/wrsrvd/src/render.rs` +- Modify: `crates/wrsrvd/src/state.rs` (add capture state) + +This task adds framebuffer capture after each render pass, using Smithay's `ExportMem` trait. The captured data is the input for network transmission in Phase 1. For now we log the capture and optionally dump to a file for verification. + +- [ ] **Step 1: Add capture infrastructure to state** + +In `crates/wrsrvd/src/state.rs`, add: + +```rust +/// Captured framebuffer data from the last render pass. +pub struct CapturedFrame { + pub data: Vec, + pub width: i32, + pub height: i32, + pub damage: Vec>, +} + +// Add to WayRay struct: +pub last_capture: Option, +``` + +- [ ] **Step 2: Add capture to render_output** + +In `crates/wrsrvd/src/render.rs`, after successful render and before `backend.submit()`, add framebuffer capture: + +```rust +use smithay::backend::renderer::ExportMem; +use smithay::backend::allocator::Fourcc; +use smithay::utils::Rectangle; + +// Inside the Ok(render_output_result) branch, before submit: +if has_damage { + // Capture the framebuffer + let output_size = output.current_mode().unwrap().size; + let region = Rectangle::from_loc_and_size( + (0, 0), + (output_size.w, output_size.h), + ); + + match backend.renderer().copy_framebuffer( + &backend.framebuffer(), + region, + Fourcc::Argb8888, + ) { + Ok(mapping) => { + match backend.renderer().map_texture(&mapping) { + Ok(pixels) => { + tracing::debug!( + width = output_size.w, + height = output_size.h, + bytes = pixels.len(), + damage_rects = render_output_result.damage.len(), + "framebuffer captured" + ); + + state.last_capture = Some(CapturedFrame { + data: pixels.to_vec(), + width: output_size.w, + height: output_size.h, + damage: render_output_result.damage.clone(), + }); + } + Err(err) => tracing::warn!(?err, "failed to map framebuffer"), + } + } + Err(err) => tracing::warn!(?err, "failed to copy framebuffer"), + } +} +``` + +Note: The exact `copy_framebuffer` API may differ — it may require `&self` vs `&mut self`, and the framebuffer handle may come from a different method. Adjust per Smithay's actual API. The key pattern is: render → copy_framebuffer → map_texture → get `&[u8]` pixel data. + +- [ ] **Step 3: Verify capture works** + +```bash +RUST_LOG=debug cargo run --bin wrsrvd +``` + +Open a client. Expected: Debug log shows "framebuffer captured" messages with correct dimensions and byte counts (width * height * 4 for ARGB8888). + +- [ ] **Step 4: Commit** + +```bash +git add crates/wrsrvd/src/render.rs crates/wrsrvd/src/state.rs +git commit -m "Add framebuffer capture via ExportMem after render + +Captures ARGB8888 pixels from the rendered framebuffer with damage +regions. This is the input for network frame transmission in Phase 1." +``` + +--- + +## Task 9: Buffer Handler and Missing Delegates + +**Files:** +- Modify: `crates/wrsrvd/src/handlers/mod.rs` or relevant handler files + +Smithay requires several additional delegate implementations for a functional compositor. These are small but necessary — clients will fail to connect without them. + +- [ ] **Step 1: Add buffer handler** + +In `crates/wrsrvd/src/handlers/compositor.rs`: + +```rust +use smithay::{delegate_output, wayland::buffer::BufferHandler}; + +impl BufferHandler for WayRay { + fn buffer_destroyed(&mut self, _buffer: &wayland_server::protocol::wl_buffer::WlBuffer) {} +} + +delegate_output!(WayRay); +``` + +- [ ] **Step 2: Add any remaining required delegates** + +Check what's needed by attempting to compile and connect a client. Common missing delegates: + +```rust +// In appropriate handler files: +smithay::delegate_xdg_decoration!(WayRay); // if xdg_decoration feature enabled +``` + +The exact set depends on the Smithay version and features enabled. The compiler will tell you what's missing via trait bound errors. + +- [ ] **Step 3: Final integration test** + +Run the compositor and test with multiple clients: + +```bash +cargo run --bin wrsrvd & +WAYLAND_DISPLAY= foot & +WAYLAND_DISPLAY= weston-simple-egl # or another Wayland client +``` + +Expected: +- Multiple clients render in the compositor window +- Click to focus works +- Keyboard input reaches the focused client +- Debug logs show framebuffer capture on each render + +- [ ] **Step 4: Commit** + +```bash +git add crates/wrsrvd/src/ +git commit -m "Add buffer handler and remaining Wayland delegates + +Compositor now fully functional: hosts multiple Wayland clients, +renders them, handles input, captures framebuffers. Phase 0 complete." +``` + +--- + +## Notes for the Implementer + +### Smithay API Stability +Smithay's API evolves between versions. The code in this plan is based on the 0.3.x API patterns. If you encounter mismatches: +1. Check the Smallvil example: https://github.com/Smithay/smithay/tree/master/smallvil +2. Check the Anvil example: https://github.com/Smithay/smithay/tree/master/anvil +3. Use `cargo doc --open` to browse the local Smithay API docs +4. Use context7 for library documentation lookups + +### Common Pitfalls +- **Missing delegate macros**: Every Smithay handler trait needs a corresponding `delegate_*!` macro call. The compiler error will say something about `Dispatch` not being implemented. +- **Client connect failures**: If clients fail to connect, check that `add_socket_auto()` was called and `WAYLAND_DISPLAY` is set correctly. +- **Render not updating**: Make sure `send_frame` is called on surfaces after rendering, otherwise clients won't commit new buffers. +- **Import paths**: Smithay re-exports wayland-server types. Prefer `smithay::reexports::wayland_server` over depending on wayland-server directly. + +### What's Next (Phase 1) +Phase 1 builds on this foundation: +- `wayray-protocol` crate gets wire protocol message definitions +- QUIC transport layer added to `wrsrvd` (server) and `wrclient` (client) +- `CapturedFrame` data gets encoded (zstd diff) and sent over QUIC +- `wrclient` receives, decodes, and displays frames