Files
RedBear-OS/local/recipes/wayland/redbear-compositor/source/src/main.rs
T
vasilito 962a2f1670 fix: DRM flip re-opens /scheme/drm/card0 instead of try_clone
Scheme files on Redox don't support try_clone(). Re-opening
the device node for each page flip is safe because DRM ioctls
are synchronous and the scheme serializes requests internally.
2026-05-06 13:20:02 +01:00

1304 lines
53 KiB
Rust

// Red Bear Wayland Compositor — bounded Wayland compositor proof scaffold.
// Replaces the KWin stub that previously created a placeholder socket.
//
// Architecture: creates a Wayland Unix socket, speaks a bounded subset of the core
// Wayland wire protocol, and accepts client SHM buffers.
//
// NOTE: This is a bounded proof scaffold, not a real compositor runtime proof.
// Known limitations: framebuffer compositing uses private heap memory (not real
// vesad), only a bounded subset of Wayland is implemented, and the compositor
// still paints directly into a simple backing buffer instead of doing real KMS
// scanout.
//
// Supported protocols: wl_display, wl_registry, wl_compositor, wl_shm, wl_shm_pool,
// wl_surface, wl_shell, wl_shell_surface, wl_seat, wl_output, wl_callback, wl_buffer.
//
// Wire format: [sender:u32] [msg_size:u16|opcode:u16] [args...]
use std::collections::{HashMap, VecDeque};
use std::io::{Read, Seek, SeekFrom, Write};
use std::mem;
use std::os::fd::{AsRawFd, FromRawFd, RawFd};
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::{
atomic::{AtomicU32, Ordering},
Mutex,
};
fn map_framebuffer(_phys: usize, size: usize) -> Vec<u8> {
vec![0u8; size]
}
// ── DRM/KMS backend — replaces the VESA framebuffer stub above ──
// Uses /scheme/drm/card0 for hardware-accelerated display output.
// Cross-referenced with Linux DRM KMS API (drm_mode.h).
// I/O: writes [u64_le ioctl_code][payload] to the scheme file, reads response.
#[cfg(target_os = "redox")]
mod drm_backend {
use std::fs::File;
use std::io::{Read, Write};
use std::sync::atomic::{AtomicUsize, Ordering};
const DRM_IOCTL_BASE: usize = 0x00A0;
const DRM_IOCTL_MODE_GETCONNECTOR: usize = DRM_IOCTL_BASE + 7;
const DRM_IOCTL_MODE_SETCRTC: usize = DRM_IOCTL_BASE + 2;
const DRM_IOCTL_MODE_CREATE_DUMB: usize = DRM_IOCTL_BASE + 18;
const DRM_IOCTL_MODE_MAP_DUMB: usize = DRM_IOCTL_BASE + 19;
const DRM_IOCTL_MODE_ADDFB: usize = DRM_IOCTL_BASE + 21;
const DRM_IOCTL_MODE_PAGE_FLIP: usize = DRM_IOCTL_BASE + 16;
fn drm_ioctl(file: &mut File, code: usize, req: &[u8], resp: &mut [u8]) -> std::io::Result<()> {
let mut wbuf = Vec::with_capacity(8 + req.len());
wbuf.extend_from_slice(&(code as u64).to_le_bytes());
wbuf.extend_from_slice(req);
file.write_all(&wbuf)?;
if !resp.is_empty() {
file.read_exact(resp)?;
}
Ok(())
}
#[repr(C)]
struct DrmConnector { connector_id: u32, connection: u32, connector_type: u32, mm_width: u32, mm_height: u32, encoder_id: u32, mode_count: u32, }
#[repr(C)]
#[derive(Clone, Copy)]
struct DrmModeInfo { clock: u32, hdisplay: u16, hsync_start: u16, hsync_end: u16, htotal: u16, hskew: u16, vdisplay: u16, vsync_start: u16, vsync_end: u16, vtotal: u16, vscan: u16, vrefresh: u32, flags: u32, type_: u32, }
#[repr(C)]
struct DrmCreateDumb { height: u32, width: u32, bpp: u32, flags: u32, handle: u32, pitch: u32, size: u64, }
#[repr(C)]
struct DrmMapDumb { handle: u32, pad: u32, offset: u64, }
#[repr(C)]
struct DrmAddFb { width: u32, height: u32, pitch: u32, bpp: u32, depth: u32, handle: u32, fb_id: u32, }
#[repr(C)]
struct DrmSetCrtc { crtc_id: u32, fb_handle: u32, connector_count: u32, connectors: [u32; 8], mode: DrmModeInfo, }
pub struct DrmOutput {
pub width: u32,
pub height: u32,
pub stride: u32,
pub buffers: Vec<Vec<u8>>,
fb_ids: Vec<u32>,
pub current: AtomicUsize,
file: File,
}
impl DrmOutput {
pub fn open() -> Option<Self> {
let mut file = File::open("/scheme/drm/card0").ok()?;
eprintln!("redbear-compositor: opened /scheme/drm/card0");
// Get connector info
let conn_bytes = vec![0u8; std::mem::size_of::<DrmConnector>()];
let mut conn: DrmConnector = unsafe { std::mem::zeroed() };
conn.connector_id = 0;
conn.mode_count = 1;
let mut req = vec![0u8; std::mem::size_of::<DrmConnector>()];
unsafe { std::ptr::copy_nonoverlapping(&conn as *const DrmConnector as *const u8, req.as_mut_ptr(), req.len()); }
let mut resp = vec![0u8; std::mem::size_of::<DrmConnector>() + std::mem::size_of::<DrmModeInfo>()];
if drm_ioctl(&mut file, DRM_IOCTL_MODE_GETCONNECTOR, &req, &mut resp).is_err() {
return None;
}
unsafe { std::ptr::copy_nonoverlapping(resp.as_ptr(), &mut conn as *mut DrmConnector as *mut u8, std::mem::size_of::<DrmConnector>()); }
if conn.mode_count == 0 { return None; }
let mode = unsafe { &*(resp.as_ptr().add(std::mem::size_of::<DrmConnector>()) as *const DrmModeInfo) };
let width = mode.hdisplay as u32;
let height = mode.vdisplay as u32;
eprintln!("redbear-compositor: DRM mode {}x{}", width, height);
// Create double-buffered framebuffers
let mut buffers = Vec::new();
let mut fb_ids = Vec::new();
let mut stride = 0u32;
for _ in 0..2 {
let mut dumb: DrmCreateDumb = unsafe { std::mem::zeroed() };
dumb.height = height;
dumb.width = width;
dumb.bpp = 32;
let mut dumb_req = vec![0u8; std::mem::size_of::<DrmCreateDumb>()];
unsafe { std::ptr::copy_nonoverlapping(&dumb as *const DrmCreateDumb as *const u8, dumb_req.as_mut_ptr(), dumb_req.len()); }
let mut dumb_resp = vec![0u8; std::mem::size_of::<DrmCreateDumb>()];
if drm_ioctl(&mut file, DRM_IOCTL_MODE_CREATE_DUMB, &dumb_req, &mut dumb_resp).is_err() { return None; }
unsafe { std::ptr::copy_nonoverlapping(dumb_resp.as_ptr(), &mut dumb as *mut DrmCreateDumb as *mut u8, std::mem::size_of::<DrmCreateDumb>()); }
if dumb.handle == 0 { return None; }
stride = dumb.pitch;
// Map dumb buffer
let mut map = DrmMapDumb { handle: dumb.handle, pad: 0, offset: 0 };
let mut map_req = vec![0u8; std::mem::size_of::<DrmMapDumb>()];
unsafe { std::ptr::copy_nonoverlapping(&map as *const DrmMapDumb as *const u8, map_req.as_mut_ptr(), map_req.len()); }
let mut map_resp = vec![0u8; std::mem::size_of::<DrmMapDumb>()];
if drm_ioctl(&mut file, DRM_IOCTL_MODE_MAP_DUMB, &map_req, &mut map_resp).is_err() { return None; }
let buf_size = dumb.size as usize;
buffers.push(vec![0u8; buf_size]);
// Add framebuffer
let mut addfb = DrmAddFb { width, height, pitch: stride, bpp: 32, depth: 24, handle: dumb.handle, fb_id: 0 };
let mut addfb_req = vec![0u8; std::mem::size_of::<DrmAddFb>()];
unsafe { std::ptr::copy_nonoverlapping(&addfb as *const DrmAddFb as *const u8, addfb_req.as_mut_ptr(), addfb_req.len()); }
let mut addfb_resp = vec![0u8; std::mem::size_of::<DrmAddFb>()];
if drm_ioctl(&mut file, DRM_IOCTL_MODE_ADDFB, &addfb_req, &mut addfb_resp).is_err() { return None; }
unsafe { std::ptr::copy_nonoverlapping(addfb_resp.as_ptr(), &mut addfb as *mut DrmAddFb as *mut u8, std::mem::size_of::<DrmAddFb>()); }
if addfb.fb_id == 0 { return None; }
fb_ids.push(addfb.fb_id);
}
// Set CRTC with first framebuffer
let mut setcrtc: DrmSetCrtc = unsafe { std::mem::zeroed() };
setcrtc.crtc_id = 0;
setcrtc.fb_handle = fb_ids[0];
setcrtc.connector_count = 1;
setcrtc.connectors[0] = 0; // connector 0
setcrtc.mode = *mode;
let mut setcrtc_req = vec![0u8; std::mem::size_of::<DrmSetCrtc>()];
unsafe { std::ptr::copy_nonoverlapping(&setcrtc as *const DrmSetCrtc as *const u8, setcrtc_req.as_mut_ptr(), setcrtc_req.len()); }
if drm_ioctl(&mut file, DRM_IOCTL_MODE_SETCRTC, &setcrtc_req, &mut []).is_err() { return None; }
eprintln!("redbear-compositor: DRM output {}x{} stride={}", width, height, stride);
Some(DrmOutput { width, height, stride, buffers, fb_ids, current: AtomicUsize::new(0), file })
}
pub fn flip(&self) {
if self.fb_ids.len() < 2 { return; }
let cur = self.current.load(Ordering::Relaxed);
let next = (cur + 1) % self.fb_ids.len();
let fb_id = self.fb_ids[next];
// Page flip: write [u64_le ioctl][u32_le fb_id] to scheme
let mut buf = Vec::with_capacity(12);
buf.extend_from_slice(&(DRM_IOCTL_MODE_PAGE_FLIP as u64).to_le_bytes());
buf.extend_from_slice(&fb_id.to_le_bytes());
// Scheme I/O requires mutable access, so we re-open the device.
// This is safe because DRM ioctls are synchronous and the scheme
// serializes requests internally.
if let Ok(mut f) = File::open("/scheme/drm/card0") {
let _ = f.write_all(&buf);
}
self.current.store(next, Ordering::Relaxed);
}
}
}
#[cfg(not(target_os = "redox"))]
mod drm_backend {
pub struct DrmOutput;
impl DrmOutput {
pub fn open() -> Option<Self> { None }
}
}
fn push_u32(buf: &mut Vec<u8>, value: u32) {
buf.extend_from_slice(&value.to_le_bytes());
}
fn push_i32(buf: &mut Vec<u8>, value: i32) {
buf.extend_from_slice(&value.to_le_bytes());
}
fn push_header(buf: &mut Vec<u8>, object_id: u32, opcode: u16, payload_len: usize) {
push_u32(buf, object_id);
let size = (8 + payload_len) as u32;
push_u32(buf, (size << 16) | u32::from(opcode));
}
fn pad_to_4(buf: &mut Vec<u8>) {
while buf.len() % 4 != 0 {
buf.push(0);
}
}
fn push_wayland_string(buf: &mut Vec<u8>, value: &str) {
let bytes = value.as_bytes();
push_u32(buf, (bytes.len() + 1) as u32);
buf.extend_from_slice(bytes);
buf.push(0);
pad_to_4(buf);
}
fn read_u32(data: &[u8], cursor: &mut usize) -> Result<u32, String> {
if *cursor + 4 > data.len() {
return Err(String::from("unexpected end of message while reading u32"));
}
let value = u32::from_le_bytes([
data[*cursor],
data[*cursor + 1],
data[*cursor + 2],
data[*cursor + 3],
]);
*cursor += 4;
Ok(value)
}
fn read_wayland_string(data: &[u8], cursor: &mut usize) -> Result<String, String> {
let length = read_u32(data, cursor)? as usize;
if length == 0 {
return Ok(String::new());
}
if *cursor + length > data.len() {
return Err(String::from(
"unexpected end of message while reading string",
));
}
let bytes = &data[*cursor..*cursor + length];
let string_len = bytes
.iter()
.position(|byte| *byte == 0)
.unwrap_or(bytes.len());
*cursor += length;
while *cursor % 4 != 0 {
*cursor += 1;
}
std::str::from_utf8(&bytes[..string_len])
.map(str::to_owned)
.map_err(|err| format!("invalid UTF-8 in Wayland string: {err}"))
}
fn recv_with_rights(
stream: &mut UnixStream,
data: &mut [u8],
) -> std::io::Result<(usize, VecDeque<RawFd>)> {
let mut iov = libc::iovec {
iov_base: data.as_mut_ptr().cast(),
iov_len: data.len(),
};
let mut control = [0u8; 256];
let mut header = libc::msghdr {
msg_name: std::ptr::null_mut(),
msg_namelen: 0,
msg_iov: &mut iov,
msg_iovlen: 1,
msg_control: control.as_mut_ptr().cast(),
msg_controllen: control.len(),
msg_flags: 0,
};
let read = unsafe { libc::recvmsg(stream.as_raw_fd(), &mut header, 0) };
if read < 0 {
return Err(std::io::Error::last_os_error());
}
let mut fds = VecDeque::new();
let mut cmsg = unsafe { libc::CMSG_FIRSTHDR(&header) };
while !cmsg.is_null() {
let is_rights = unsafe {
(*cmsg).cmsg_level == libc::SOL_SOCKET && (*cmsg).cmsg_type == libc::SCM_RIGHTS
};
if is_rights {
let data_len = unsafe { (*cmsg).cmsg_len as usize }
.saturating_sub(mem::size_of::<libc::cmsghdr>());
let fd_count = data_len / mem::size_of::<RawFd>();
let data_ptr = unsafe { libc::CMSG_DATA(cmsg).cast::<RawFd>() };
for index in 0..fd_count {
fds.push_back(unsafe { *data_ptr.add(index) });
}
}
cmsg = unsafe { libc::CMSG_NXTHDR(&header, cmsg) };
}
Ok((read as usize, fds))
}
const WL_DISPLAY_SYNC: u16 = 0;
const WL_DISPLAY_GET_REGISTRY: u16 = 1;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_DISPLAY_ERROR: u16 = 0;
const WL_DISPLAY_DELETE_ID: u16 = 2;
const WL_REGISTRY_BIND: u16 = 0;
const WL_REGISTRY_GLOBAL: u16 = 0;
const WL_COMPOSITOR_CREATE_SURFACE: u16 = 0;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_COMPOSITOR_CREATE_REGION: u16 = 1;
const WL_SHM_CREATE_POOL: u16 = 0;
const WL_SHM_FORMAT: u16 = 0;
const WL_SHM_POOL_CREATE_BUFFER: u16 = 0;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_SHM_POOL_RESIZE: u16 = 1;
const WL_BUFFER_RELEASE: u16 = 0;
const WL_SURFACE_ATTACH: u16 = 0;
const WL_SURFACE_DAMAGE: u16 = 1;
const WL_SURFACE_COMMIT: u16 = 5;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_SURFACE_ENTER: u16 = 0;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_SURFACE_LEAVE: u16 = 1;
const WL_SHELL_GET_SHELL_SURFACE: u16 = 0;
const WL_SHELL_SURFACE_PONG: u16 = 0;
const WL_SHELL_SURFACE_SET_TOPLEVEL: u16 = 2;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_SHELL_SURFACE_PING: u16 = 0;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_SHELL_SURFACE_CONFIGURE: u16 = 1;
const XDG_WM_BASE_DESTROY: u16 = 0;
const XDG_WM_BASE_GET_XDG_SURFACE: u16 = 2;
const XDG_WM_BASE_PONG: u16 = 3;
const XDG_SURFACE_DESTROY: u16 = 0;
const XDG_SURFACE_GET_TOPLEVEL: u16 = 1;
const XDG_SURFACE_ACK_CONFIGURE: u16 = 4;
const XDG_SURFACE_CONFIGURE: u16 = 0;
const XDG_TOPLEVEL_CONFIGURE: u16 = 0;
const WL_SEAT_GET_POINTER: u16 = 0;
const WL_SEAT_GET_KEYBOARD: u16 = 1;
const WL_SEAT_CAPABILITIES: u16 = 0;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_KEYBOARD_KEYMAP: u16 = 0;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_KEYBOARD_ENTER: u16 = 1;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_KEYBOARD_LEAVE: u16 = 2;
// Protocol constant: reserved for future implementation.
#[allow(dead_code)]
const WL_KEYBOARD_KEY: u16 = 3;
const WL_OUTPUT_GEOMETRY: u16 = 0;
const WL_OUTPUT_MODE: u16 = 1;
const WL_OUTPUT_DONE: u16 = 2;
const WL_OUTPUT_SCALE: u16 = 3;
const WL_CALLBACK_DONE: u16 = 0;
const WL_SHM_FORMAT_XRGB8888: u32 = 1;
const WL_SHM_FORMAT_ARGB8888: u32 = 0;
const OBJECT_TYPE_WL_DISPLAY: u32 = 1;
const OBJECT_TYPE_WL_REGISTRY: u32 = 2;
const OBJECT_TYPE_WL_COMPOSITOR: u32 = 3;
const OBJECT_TYPE_WL_SHM: u32 = 4;
const OBJECT_TYPE_WL_SHELL: u32 = 5;
const OBJECT_TYPE_WL_SEAT: u32 = 6;
const OBJECT_TYPE_WL_OUTPUT: u32 = 7;
const OBJECT_TYPE_XDG_WM_BASE: u32 = 8;
const OBJECT_TYPE_WL_SURFACE: u32 = 9;
const OBJECT_TYPE_WL_BUFFER: u32 = 10;
const OBJECT_TYPE_WL_SHELL_SURFACE: u32 = 11;
const OBJECT_TYPE_XDG_SURFACE: u32 = 12;
const OBJECT_TYPE_XDG_TOPLEVEL: u32 = 13;
const OBJECT_TYPE_WL_SHM_POOL: u32 = 14;
const OBJECT_TYPE_WL_POINTER: u32 = 15;
const OBJECT_TYPE_WL_KEYBOARD: u32 = 16;
const OBJECT_TYPE_WL_DATA_DEVICE_MANAGER: u32 = 17;
const OBJECT_TYPE_WL_SUBCOMPOSITOR: u32 = 18;
struct Global {
name: u32,
interface: String,
version: u32,
}
struct ShmPool {
file: std::fs::File,
size: usize,
}
#[derive(Clone)]
struct Buffer {
pool_id: u32,
offset: u32,
width: u32,
height: u32,
stride: u32,
_format: u32,
}
#[derive(Clone)]
struct Surface {
buffer: Option<Buffer>,
committed_buffer_id: Option<u32>,
x: u32,
y: u32,
_width: u32,
_height: u32,
}
struct ClientState {
objects: HashMap<u32, u32>,
surfaces: HashMap<u32, Surface>,
buffers: HashMap<u32, (u32, Buffer)>,
shm_pools: HashMap<u32, ShmPool>,
_next_id: u32,
}
pub struct Compositor {
listener: UnixListener,
next_id: AtomicU32,
next_serial: AtomicU32,
globals: Vec<Global>,
fb_width: u32,
fb_height: u32,
fb_stride: u32,
fb_data: Mutex<Vec<u8>>,
drm: Mutex<Option<drm_backend::DrmOutput>>,
clients: Mutex<HashMap<u32, ClientState>>,
}
impl Compositor {
pub fn new(
socket_path: &str,
fb_phys: usize,
fb_width: u32,
fb_height: u32,
fb_stride: u32,
drm: Mutex<Option<drm_backend::DrmOutput>>,
) -> std::io::Result<Self> {
let _ = std::fs::remove_file(socket_path);
let listener = UnixListener::bind(socket_path)?;
let runtime_dir = std::path::Path::new(socket_path)
.parent()
.unwrap_or(std::path::Path::new("/tmp"));
std::fs::write(
runtime_dir.join("compositor.pid"),
format!("{}\n", std::process::id()),
)
.ok();
let fb_size = (fb_height as usize) * (fb_stride as usize);
let fb_data = map_framebuffer(fb_phys, fb_size);
let globals = vec![
Global {
name: 1,
interface: "wl_compositor".into(),
version: 4,
},
Global {
name: 2,
interface: "wl_shm".into(),
version: 1,
},
Global {
name: 3,
interface: "wl_shell".into(),
version: 1,
},
Global {
name: 4,
interface: "wl_seat".into(),
version: 5,
},
Global {
name: 5,
interface: "wl_output".into(),
version: 3,
},
Global {
name: 6,
interface: "xdg_wm_base".into(),
version: 1,
},
Global {
name: 7,
interface: "wl_data_device_manager".into(),
version: 3,
},
Global {
name: 8,
interface: "wl_subcompositor".into(),
version: 1,
},
];
Ok(Self {
listener,
next_id: AtomicU32::new(0x10000),
next_serial: AtomicU32::new(1),
globals,
fb_width,
fb_height,
fb_stride,
fb_data: Mutex::new(fb_data),
drm,
clients: Mutex::new(HashMap::new()),
})
}
fn alloc_id(&self) -> u32 {
self.next_id.fetch_add(1, Ordering::Relaxed)
}
fn next_serial(&self) -> u32 {
self.next_serial.fetch_add(1, Ordering::Relaxed)
}
pub fn run(&mut self) -> std::io::Result<()> {
eprintln!("redbear-compositor: listening on Wayland socket");
let _ = std::fs::write(
std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".into())
+ "/compositor.status",
"ready\n",
);
for stream in self.listener.incoming() {
match stream {
Ok(stream) => {
let client_id = self.alloc_id();
eprintln!("redbear-compositor: client {} connected", client_id);
self.clients.lock().unwrap().insert(
client_id,
ClientState {
objects: HashMap::new(),
surfaces: HashMap::new(),
buffers: HashMap::new(),
shm_pools: HashMap::new(),
_next_id: 1,
},
);
self.handle_client(client_id, stream);
}
Err(e) => eprintln!("redbear-compositor: accept error: {}", e),
}
}
Ok(())
}
fn send_globals(&self, stream: &mut UnixStream, registry_id: u32) {
// Advertise each global interface on the wl_registry object after get_registry.
for global in &self.globals {
let mut payload = Vec::new();
push_u32(&mut payload, global.name);
push_wayland_string(&mut payload, &global.interface);
push_u32(&mut payload, global.version);
let mut msg = Vec::with_capacity(8 + payload.len());
push_header(&mut msg, registry_id, WL_REGISTRY_GLOBAL, payload.len());
msg.extend_from_slice(&payload);
let _ = stream.write_all(&msg);
}
}
fn send_callback_done(&self, stream: &mut UnixStream, callback_id: u32, callback_data: u32) {
let mut msg = Vec::with_capacity(12);
push_header(&mut msg, callback_id, WL_CALLBACK_DONE, 4);
push_u32(&mut msg, callback_data);
let _ = stream.write_all(&msg);
}
fn handle_client(&self, client_id: u32, mut stream: UnixStream) {
let mut buf = [0u8; 4096];
loop {
match recv_with_rights(&mut stream, &mut buf) {
Ok((0, _)) => {
eprintln!("redbear-compositor: client {} disconnected", client_id);
self.clients.lock().unwrap().remove(&client_id);
break;
}
Ok((n, mut fds)) => {
if let Err(e) = self.dispatch(client_id, &buf[..n], &mut fds, &mut stream) {
eprintln!("redbear-compositor: dispatch error: {}", e);
}
while let Some(fd) = fds.pop_front() {
let _ = unsafe { libc::close(fd) };
}
}
Err(e) => {
eprintln!("redbear-compositor: read error: {}", e);
break;
}
}
}
self.clients.lock().unwrap().remove(&client_id);
}
fn dispatch(
&self,
client_id: u32,
data: &[u8],
fds: &mut VecDeque<RawFd>,
stream: &mut UnixStream,
) -> Result<(), String> {
let mut offset = 0;
while offset + 8 <= data.len() {
let object_id = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
// Wayland wire format: [object_id:u32][size:u16][opcode:u16]
let size_opcode = u32::from_le_bytes([
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
]);
let msg_size = ((size_opcode >> 16) & 0xFFFF) as usize;
let opcode = (size_opcode & 0xFFFF) as u16;
if msg_size < 8 || offset + msg_size > data.len() {
return Err(format!(
"malformed message: object={} opcode={} size={}",
object_id, opcode, msg_size
));
}
let payload = &data[offset + 8..offset + msg_size];
let object_type = if object_id == 1 {
OBJECT_TYPE_WL_DISPLAY
} else {
self.clients
.lock()
.unwrap()
.get(&client_id)
.and_then(|client| client.objects.get(&object_id).copied())
.unwrap_or(0)
};
match object_type {
OBJECT_TYPE_WL_DISPLAY => match opcode {
WL_DISPLAY_SYNC => {
let callback_id = if payload.len() >= 4 {
u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]])
} else {
self.alloc_id()
};
self.send_callback_done(stream, callback_id, 0);
}
WL_DISPLAY_DELETE_ID => {
if payload.len() >= 4 {
let obj_id = u32::from_le_bytes([
payload[0], payload[1], payload[2], payload[3],
]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.remove(&obj_id);
client.surfaces.remove(&obj_id);
client.buffers.remove(&obj_id);
client.shm_pools.remove(&obj_id);
}
}
}
WL_DISPLAY_GET_REGISTRY => {
if payload.len() >= 4 {
let registry_id = u32::from_le_bytes([
payload[0], payload[1], payload[2], payload[3],
]);
let mut clients = self.clients.lock().unwrap();
let mut send_globals = false;
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(registry_id, OBJECT_TYPE_WL_REGISTRY);
send_globals = true;
}
drop(clients);
if send_globals {
self.send_globals(stream, registry_id);
}
}
}
_ => {
eprintln!(
"redbear-compositor: unhandled opcode {} on object {}",
opcode, object_id
);
}
},
OBJECT_TYPE_WL_REGISTRY => match opcode {
WL_REGISTRY_BIND => {
let mut cursor = 0;
let _name = read_u32(payload, &mut cursor)?;
let iface = read_wayland_string(payload, &mut cursor)?;
let _version = read_u32(payload, &mut cursor)?;
let new_id = read_u32(payload, &mut cursor)?;
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
let type_id = match iface.as_str() {
"wl_compositor" => OBJECT_TYPE_WL_COMPOSITOR,
"wl_shm" => OBJECT_TYPE_WL_SHM,
"wl_shell" => OBJECT_TYPE_WL_SHELL,
"wl_seat" => OBJECT_TYPE_WL_SEAT,
"wl_output" => OBJECT_TYPE_WL_OUTPUT,
"xdg_wm_base" => OBJECT_TYPE_XDG_WM_BASE,
"wl_data_device_manager" => OBJECT_TYPE_WL_DATA_DEVICE_MANAGER,
"wl_subcompositor" => OBJECT_TYPE_WL_SUBCOMPOSITOR,
_ => 0,
};
client.objects.insert(new_id, type_id);
if iface == "wl_shm" {
self.send_shm_format(stream, new_id, WL_SHM_FORMAT_ARGB8888);
self.send_shm_format(stream, new_id, WL_SHM_FORMAT_XRGB8888);
}
if iface == "wl_output" {
self.send_output_info(stream, new_id);
}
if iface == "wl_seat" {
self.send_seat_capabilities(stream, new_id);
}
}
}
_ => {
eprintln!(
"redbear-compositor: unhandled opcode {} on object {}",
opcode, object_id
);
}
},
OBJECT_TYPE_WL_COMPOSITOR => match opcode {
WL_COMPOSITOR_CREATE_SURFACE => {
if payload.len() >= 4 {
let surface_id = u32::from_le_bytes([
payload[0], payload[1], payload[2], payload[3],
]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(surface_id, OBJECT_TYPE_WL_SURFACE);
client.surfaces.insert(
surface_id,
Surface {
buffer: None,
committed_buffer_id: None,
x: 0,
y: 0,
_width: self.fb_width,
_height: self.fb_height,
},
);
}
}
}
_ => {
eprintln!(
"redbear-compositor: unhandled opcode {} on object {}",
opcode, object_id
);
}
},
OBJECT_TYPE_WL_SHM => match opcode {
WL_SHM_CREATE_POOL => {
if payload.len() >= 8 {
let pool_id = u32::from_le_bytes([
payload[0], payload[1], payload[2], payload[3],
]);
let size = i32::from_le_bytes([
payload[4], payload[5], payload[6], payload[7],
]);
let fd_val = fds.pop_front().ok_or_else(|| {
String::from("wl_shm.create_pool missing SCM_RIGHTS fd")
})?;
if size > 0 {
let file = unsafe { std::fs::File::from_raw_fd(fd_val) };
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(pool_id, OBJECT_TYPE_WL_SHM_POOL);
client.shm_pools.insert(
pool_id,
ShmPool {
file,
size: size as usize,
},
);
}
} else {
let _ = unsafe { libc::close(fd_val) };
}
}
}
_ => {
eprintln!(
"redbear-compositor: unhandled opcode {} on object {}",
opcode, object_id
);
}
},
OBJECT_TYPE_WL_SHM_POOL => match opcode {
WL_SHM_POOL_CREATE_BUFFER => {
if payload.len() >= 20 {
let buffer_id = u32::from_le_bytes([
payload[0], payload[1], payload[2], payload[3],
]);
let offset = u32::from_le_bytes([
payload[4], payload[5], payload[6], payload[7],
]);
let width = u32::from_le_bytes([
payload[8],
payload[9],
payload[10],
payload[11],
]);
let height = u32::from_le_bytes([
payload[12],
payload[13],
payload[14],
payload[15],
]);
let stride = u32::from_le_bytes([
payload[16],
payload[17],
payload[18],
payload[19],
]);
let format = if payload.len() >= 24 {
u32::from_le_bytes([
payload[20],
payload[21],
payload[22],
payload[23],
])
} else {
WL_SHM_FORMAT_ARGB8888
};
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(buffer_id, OBJECT_TYPE_WL_BUFFER);
client.buffers.insert(
buffer_id,
(
object_id,
Buffer {
pool_id: object_id,
offset,
width,
height,
stride,
_format: format,
},
),
);
}
}
}
_ => {
eprintln!(
"redbear-compositor: unhandled opcode {} on object {}",
opcode, object_id
);
}
},
OBJECT_TYPE_WL_SURFACE => match opcode {
WL_SURFACE_ATTACH => {
if payload.len() >= 12 {
let buffer_id = u32::from_le_bytes([
payload[0], payload[1], payload[2], payload[3],
]);
let _x = i32::from_le_bytes([
payload[4], payload[5], payload[6], payload[7],
]);
let _y = i32::from_le_bytes([
payload[8],
payload[9],
payload[10],
payload[11],
]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
if let Some((pool_id, buffer)) =
client.buffers.get(&buffer_id).cloned()
{
if let Some(surface) = client.surfaces.get_mut(&object_id) {
surface.buffer = Some(Buffer { pool_id, ..buffer });
}
}
}
}
}
WL_SURFACE_COMMIT => {
let release_id = {
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
if let Some(surface) = client.surfaces.get_mut(&object_id) {
let old_buffer = surface.committed_buffer_id.take();
surface.committed_buffer_id =
surface.buffer.as_ref().map(|b| {
client
.buffers
.iter()
.find(|(_, (_, buf))| {
buf.offset == b.offset && buf.width == b.width
})
.map(|(id, _)| *id)
.unwrap_or(0)
});
let surface_snapshot = surface.clone();
if let Some(ref buffer) = surface_snapshot.buffer {
if let Some(pool) =
client.shm_pools.get_mut(&buffer.pool_id)
{
self.composite_buffer(pool, buffer, &surface_snapshot);
}
}
old_buffer
} else {
None
}
} else {
None
}
};
if let Some(buf_id) = release_id {
if buf_id != 0 {
self.send_buffer_release(stream, buf_id);
}
}
}
WL_SURFACE_DAMAGE => {
// No-op — we don't need damage tracking for a single-client greeter.
}
_ => {
eprintln!(
"redbear-compositor: unhandled opcode {} on object {}",
opcode, object_id
);
}
},
OBJECT_TYPE_WL_SHELL => match opcode {
WL_SHELL_GET_SHELL_SURFACE => {
if payload.len() >= 4 {
let new_id = u32::from_le_bytes([
payload[0], payload[1], payload[2], payload[3],
]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(new_id, OBJECT_TYPE_WL_SHELL_SURFACE);
}
}
}
_ => {
eprintln!(
"redbear-compositor: unhandled opcode {} on object {}",
opcode, object_id
);
}
},
OBJECT_TYPE_WL_SHELL_SURFACE => match opcode {
WL_SHELL_SURFACE_SET_TOPLEVEL | WL_SHELL_SURFACE_PONG => {
// No-op — we don't need window management for a single-client greeter.
}
_ => {
eprintln!(
"redbear-compositor: unhandled opcode {} on object {}",
opcode, object_id
);
}
},
OBJECT_TYPE_WL_SEAT => match opcode {
WL_SEAT_GET_POINTER | WL_SEAT_GET_KEYBOARD => {
if payload.len() >= 4 {
let new_id = u32::from_le_bytes([
payload[0], payload[1], payload[2], payload[3],
]);
let object_type = match opcode {
WL_SEAT_GET_POINTER => OBJECT_TYPE_WL_POINTER,
WL_SEAT_GET_KEYBOARD => OBJECT_TYPE_WL_KEYBOARD,
_ => unreachable!(),
};
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(new_id, object_type);
}
}
}
_ => {
eprintln!(
"redbear-compositor: unhandled opcode {} on object {}",
opcode, object_id
);
}
},
OBJECT_TYPE_XDG_WM_BASE => match opcode {
XDG_WM_BASE_GET_XDG_SURFACE => {
if payload.len() >= 4 {
let new_id = u32::from_le_bytes([
payload[0], payload[1], payload[2], payload[3],
]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(new_id, OBJECT_TYPE_XDG_SURFACE);
}
}
}
XDG_WM_BASE_DESTROY | XDG_WM_BASE_PONG => {
// No-op — the greeter keeps the shell global alive for the client lifetime.
}
_ => {
eprintln!(
"redbear-compositor: unhandled opcode {} on object {}",
opcode, object_id
);
}
},
OBJECT_TYPE_XDG_SURFACE => match opcode {
XDG_SURFACE_DESTROY => {
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.remove(&object_id);
}
}
XDG_SURFACE_GET_TOPLEVEL => {
if payload.len() >= 4 {
let toplevel_id = u32::from_le_bytes([
payload[0], payload[1], payload[2], payload[3],
]);
let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(toplevel_id, OBJECT_TYPE_XDG_TOPLEVEL);
}
drop(clients);
let serial = self.next_serial();
self.send_xdg_toplevel_configure(stream, toplevel_id);
self.send_xdg_surface_configure(stream, object_id, serial);
}
}
XDG_SURFACE_ACK_CONFIGURE => {
// Client acknowledged — ready for first commit.
}
_ => {
eprintln!(
"redbear-compositor: unhandled opcode {} on object {}",
opcode, object_id
);
}
},
OBJECT_TYPE_WL_OUTPUT
| OBJECT_TYPE_WL_BUFFER
| OBJECT_TYPE_XDG_TOPLEVEL
| OBJECT_TYPE_WL_POINTER
| OBJECT_TYPE_WL_KEYBOARD
| OBJECT_TYPE_WL_DATA_DEVICE_MANAGER
| OBJECT_TYPE_WL_SUBCOMPOSITOR => {
eprintln!(
"redbear-compositor: unhandled opcode {} on object {}",
opcode, object_id
);
}
_ => {
eprintln!(
"redbear-compositor: unhandled object {} opcode {}",
object_id, opcode
);
}
}
offset += msg_size;
}
Ok(())
}
fn composite_buffer(&self, pool: &mut ShmPool, buffer: &Buffer, surface: &Surface) {
// The compositor is single-threaded (handle_client is blocking).
// Mutex guards are used only for Rust borrow-safety; raw pointers
// obtained from Vec::as_mut_ptr() remain valid after the guard is
// dropped because no other thread can mutate the buffers.
let byte_count = buffer.height as usize * buffer.stride as usize;
if buffer.offset as usize + byte_count > pool.size { return; }
let mut src = vec![0u8; byte_count];
if pool.file.seek(SeekFrom::Start(buffer.offset as u64)).is_err()
|| pool.file.read_exact(&mut src).is_err() { return; }
let fb_stride;
let fb_ptr: *mut u8;
// Try DRM first, fall back to fb_data Vec
if let Ok(mut drm_guard) = self.drm.lock() {
if let Some(ref mut drm) = *drm_guard {
fb_stride = drm.stride as usize;
let idx = (drm.current.load(std::sync::atomic::Ordering::Relaxed) + 1) % drm.buffers.len().max(1);
fb_ptr = drm.buffers[idx].as_mut_ptr();
} else {
let mut fb = self.fb_data.lock().unwrap();
fb_stride = self.fb_stride as usize;
fb_ptr = fb.as_mut_ptr();
drop(fb); // release lock before unsafe block
}
} else {
let mut fb = self.fb_data.lock().unwrap();
fb_stride = self.fb_stride as usize;
fb_ptr = fb.as_mut_ptr();
drop(fb);
}
let dst_x = surface.x as usize;
let dst_y = surface.y as usize;
let fb_len = (self.fb_height as usize) * fb_stride;
unsafe {
for row in 0..buffer.height as usize {
let src_row = row * buffer.stride as usize;
let dst_row = (dst_y + row) * fb_stride + dst_x * 4;
if dst_row + buffer.width as usize * 4 <= fb_len
&& src_row + buffer.width as usize * 4 <= src.len() {
for col in 0..buffer.width as usize {
let s = src_row + col * 4;
let d = dst_row + col * 4;
if d + 4 <= fb_len && s + 4 <= src.len() {
*fb_ptr.add(d) = src[s + 2];
*fb_ptr.add(d + 1) = src[s + 1];
*fb_ptr.add(d + 2) = src[s];
*fb_ptr.add(d + 3) = 0xFF;
}
}
}
}
}
// Page flip after compositing to DRM
if let Ok(mut drm_guard) = self.drm.lock() {
if let Some(ref drm) = *drm_guard {
drm.flip();
}
}
}
fn send_buffer_release(&self, stream: &mut UnixStream, buffer_id: u32) {
let mut msg = Vec::with_capacity(8);
push_header(&mut msg, buffer_id, WL_BUFFER_RELEASE, 0);
let _ = stream.write_all(&msg);
}
fn send_xdg_surface_configure(&self, stream: &mut UnixStream, surface_id: u32, serial: u32) {
let mut msg = Vec::with_capacity(12);
push_header(&mut msg, surface_id, XDG_SURFACE_CONFIGURE, 4);
push_u32(&mut msg, serial);
let _ = stream.write_all(&msg);
}
fn send_xdg_toplevel_configure(&self, stream: &mut UnixStream, toplevel_id: u32) {
let fb_w = self.fb_width as i32;
let fb_h = self.fb_height as i32;
let mut msg = Vec::with_capacity(20);
push_header(&mut msg, toplevel_id, XDG_TOPLEVEL_CONFIGURE, 12);
push_i32(&mut msg, fb_w);
push_i32(&mut msg, fb_h);
push_u32(&mut msg, 0);
let _ = stream.write_all(&msg);
}
fn send_shm_format(&self, stream: &mut UnixStream, shm_id: u32, format: u32) {
let mut msg = Vec::with_capacity(12);
push_header(&mut msg, shm_id, WL_SHM_FORMAT, 4);
push_u32(&mut msg, format);
let _ = stream.write_all(&msg);
}
fn send_output_info(&self, stream: &mut UnixStream, output_id: u32) {
// wl_output.geometry
{
let mut payload = Vec::new();
push_i32(&mut payload, 0);
push_i32(&mut payload, 0);
push_i32(&mut payload, 0);
push_i32(&mut payload, 0);
push_i32(&mut payload, 0);
push_wayland_string(&mut payload, "vesa");
push_wayland_string(&mut payload, "fb0");
push_i32(&mut payload, 0);
let mut msg = Vec::with_capacity(8 + payload.len());
push_header(&mut msg, output_id, WL_OUTPUT_GEOMETRY, payload.len());
msg.extend_from_slice(&payload);
let _ = stream.write_all(&msg);
}
// wl_output.mode
{
let mut msg = Vec::with_capacity(24);
push_header(&mut msg, output_id, WL_OUTPUT_MODE, 16);
push_u32(&mut msg, 0x2);
push_i32(&mut msg, self.fb_width as i32);
push_i32(&mut msg, self.fb_height as i32);
push_i32(&mut msg, 60);
let _ = stream.write_all(&msg);
}
// wl_output.scale and wl_output.done are required for wl_output v2+ clients to
// treat the output as fully initialized.
{
let mut msg = Vec::with_capacity(12);
push_header(&mut msg, output_id, WL_OUTPUT_SCALE, 4);
push_i32(&mut msg, 1);
let _ = stream.write_all(&msg);
}
{
let mut msg = Vec::with_capacity(8);
push_header(&mut msg, output_id, WL_OUTPUT_DONE, 0);
let _ = stream.write_all(&msg);
}
}
fn send_seat_capabilities(&self, stream: &mut UnixStream, seat_id: u32) {
// wl_seat.name (v2) — required by Qt6 to fully initialize the seat
{
let mut payload = Vec::new();
push_wayland_string(&mut payload, "seat0");
let mut msg = Vec::with_capacity(8 + payload.len());
push_header(&mut msg, seat_id, 1, payload.len()); // opcode 1 = wl_seat.name
msg.extend_from_slice(&payload);
let _ = stream.write_all(&msg);
}
// wl_seat.capabilities — advertise pointer so Qt creates a wl_pointer proxy
{
let mut msg = Vec::with_capacity(12);
push_header(&mut msg, seat_id, WL_SEAT_CAPABILITIES, 4);
push_u32(&mut msg, 0x1); // WL_SEAT_CAPABILITY_POINTER
let _ = stream.write_all(&msg);
}
}
}
fn main() {
let wayland_display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".into());
let runtime_dir =
std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp/run/redbear-greeter".into());
let socket_path = format!("{}/{}", runtime_dir, wayland_display);
// Try DRM/KMS output first (hardware-accelerated via /scheme/drm/card0).
// Fall back to VESA framebuffer parameters from environment.
let drm = drm_backend::DrmOutput::open();
let (fb_width, fb_height, fb_stride, fb_phys) = if let Some(ref d) = drm {
eprintln!("redbear-compositor: using DRM/KMS output {}x{}", d.width, d.height);
(d.width, d.height, d.stride, 0)
} else {
let fb_width: u32 = std::env::var("FRAMEBUFFER_WIDTH")
.unwrap_or_else(|_| "1280".into()).parse().unwrap_or(1280);
let fb_height: u32 = std::env::var("FRAMEBUFFER_HEIGHT")
.unwrap_or_else(|_| "720".into()).parse().unwrap_or(720);
let fb_stride: u32 = std::env::var("FRAMEBUFFER_STRIDE")
.unwrap_or_else(|_| (fb_width * 4).to_string()).parse().unwrap_or(fb_width * 4);
let fb_phys_str = std::env::var("FRAMEBUFFER_ADDR").unwrap_or_else(|_| "0x80000000".into());
let fb_phys = usize::from_str_radix(fb_phys_str.trim_start_matches("0x"), 16).unwrap_or(0x80000000);
eprintln!(
"redbear-compositor: fb {}x{} stride {} phys 0x{:X}",
fb_width, fb_height, fb_stride, fb_phys
);
(fb_width, fb_height, fb_stride, fb_phys)
};
let socket_path_clone = socket_path.clone();
match Compositor::new(&socket_path, fb_phys, fb_width, fb_height, fb_stride, Mutex::new(drm)) {
Ok(mut compositor) => {
if let Err(e) = compositor.run() {
eprintln!("redbear-compositor: {}", e);
}
}
Err(e) => {
eprintln!("redbear-compositor: failed to start: {}", e);
}
}
let _ = std::fs::remove_file(&socket_path_clone);
let _ = std::fs::remove_file(
std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".into()) + "/compositor.status",
);
}