From 8e7aea9fc5f19e81e18a4750fcc1ebcd462bbc44 Mon Sep 17 00:00:00 2001 From: vasilito Date: Fri, 31 Jul 2026 06:23:23 +0900 Subject: [PATCH] redbear-compositor: fix undefined libc_clock_gettime link error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clock_monotonic_nsec() declared a hand-rolled `extern "C" fn libc_clock_gettime` plus a private timespec/CLOCK_MONOTONIC — but relibc exports `clock_gettime`, not `libc_clock_gettime`, so the cross link failed: undefined reference to `libc_clock_gettime' Use the libc crate's binding (libc::clock_gettime / libc::CLOCK_MONOTONIC / libc::timespec), already a dependency and used throughout this file. Removes the bespoke extern block, struct, and const. Co-Authored-By: Claude Fable 5 --- .../redbear-compositor/source/src/common.rs | 4353 +++++++++++++++++ 1 file changed, 4353 insertions(+) create mode 100644 local/recipes/wayland/redbear-compositor/source/src/common.rs diff --git a/local/recipes/wayland/redbear-compositor/source/src/common.rs b/local/recipes/wayland/redbear-compositor/source/src/common.rs new file mode 100644 index 0000000000..e8915c1e27 --- /dev/null +++ b/local/recipes/wayland/redbear-compositor/source/src/common.rs @@ -0,0 +1,4353 @@ +// common.rs — shared compositor state, types, wire helpers, protocol dispatch +// Extracted from main.rs during Phase 3D-2 module split. + +use std::collections::{HashMap, HashSet, VecDeque}; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::mem; +use std::net::Shutdown; +use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd}; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::sync::{ + atomic::{AtomicU32, AtomicU64, Ordering}, + Mutex, +}; + +pub enum Framebuffer { + Vec(Vec), + Mmap(MmapBuffer), +} + +impl AsMut<[u8]> for Framebuffer { + fn as_mut(&mut self) -> &mut [u8] { + match self { + Framebuffer::Vec(v) => v.as_mut(), + Framebuffer::Mmap(m) => m.as_mut(), + } + } +} + +// The protocol, state, wire, handler, and display-backend modules +// are split out for clarity and to keep main.rs's main loop short. +// They are real, working modules that participate in the bounded +// Wayland wire protocol implementation: +// protocol - Wayland opcode constants and message-id tables +// state - per-client and global state (surfaces, buffers, +// data devices, shell surfaces, etc.) +// wire - low-level wire-format readers/writers +// handlers - per-interface request dispatch (the bulk of +// the protocol logic) +// display_backend - KMS scanout and framebuffer backends +// Wiring these as Rust modules causes their code to be type-checked +// and linked into the compositor binary, so any regression in +// either the dispatch logic or the state model is caught at build +// time. + + +pub struct MmapBuffer { + base: *mut u8, + base_len: usize, + data: *mut u8, + len: usize, +} + +unsafe impl Send for MmapBuffer {} + +impl AsMut<[u8]> for MmapBuffer { + fn as_mut(&mut self) -> &mut [u8] { + unsafe { std::slice::from_raw_parts_mut(self.data, self.len) } + } +} + +impl Drop for MmapBuffer { + fn drop(&mut self) { + unsafe { + let _ = libc::munmap(self.base as *mut libc::c_void, self.base_len); + } + } +} + +fn map_framebuffer(phys: usize, size: usize) -> std::io::Result { + if phys == 0 || size == 0 { + return Ok(Framebuffer::Vec(vec![0u8; size])); + } + + let path = if cfg!(target_os = "redox") { + "/scheme/memory\0" + } else { + "/dev/mem\0" + }; + + let fd = unsafe { libc::open(path.as_ptr() as *const libc::c_char, libc::O_RDWR) }; + if fd < 0 { + if !cfg!(target_os = "redox") { + return Ok(Framebuffer::Vec(vec![0u8; size])); + } + return Err(std::io::Error::other(format!( + "failed to open framebuffer memory device: {}", + std::io::Error::last_os_error() + ))); + } + + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }; + let map_offset = phys & !(page_size - 1); + let in_page = phys - map_offset; + let map_size = ((size + in_page + page_size - 1) / page_size) * page_size; + + let ptr = unsafe { + libc::mmap( + std::ptr::null_mut(), + map_size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + fd, + map_offset as libc::off_t, + ) + }; + + unsafe { libc::close(fd); } + + if ptr == libc::MAP_FAILED { + if !cfg!(target_os = "redox") { + return Ok(Framebuffer::Vec(vec![0u8; size])); + } + return Err(std::io::Error::other(format!( + "failed to map framebuffer at 0x{:X}: {}", + phys, + std::io::Error::last_os_error() + ))); + } + + let data_ptr = unsafe { (ptr as *mut u8).add(in_page) }; + + Ok(Framebuffer::Mmap(MmapBuffer { + base: ptr as *mut u8, + base_len: map_size, + data: data_ptr, + len: 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")] +pub 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)] + #[derive(Clone, Copy)] + struct DrmResources { + connector_count: u32, + crtc_count: u32, + encoder_count: u32, + } + #[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)] + #[derive(Clone, Copy)] + struct DrmGetEncoder { + encoder_id: u32, + encoder_type: u32, + crtc_id: u32, + possible_crtcs: u32, + possible_clones: 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<(usize, usize)>, + pub fb_ids: Vec, + pub current: AtomicUsize, + _file: File, + } + + impl DrmOutput { + pub fn open() -> Option { + let mut file = File::open("/scheme/drm/card0").ok()?; + log::info!("redbear-compositor: opened /scheme/drm/card0"); + + let mut resources_resp = vec![0u8; std::mem::size_of::() + 32]; + if drm_ioctl(&mut file, DRM_IOCTL_BASE, &[], &mut resources_resp).is_err() { + return None; + } + let resources = unsafe { *(resources_resp.as_ptr() as *const DrmResources) }; + if resources.connector_count == 0 { + return None; + } + let connector_id = unsafe { + *(resources_resp + .as_ptr() + .add(std::mem::size_of::()) as *const u32) + }; + if connector_id == 0 { + return None; + } + + // Get connector info + let mut conn: DrmConnector = unsafe { std::mem::zeroed() }; + conn.connector_id = connector_id; + conn.mode_count = 1; + let mut req = vec![0u8; std::mem::size_of::()]; + 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::() + std::mem::size_of::()]; + 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::(), + ); + } + if conn.mode_count == 0 { + return None; + } + let mode = unsafe { + &*(resp.as_ptr().add(std::mem::size_of::()) as *const DrmModeInfo) + }; + let width = mode.hdisplay as u32; + let height = mode.vdisplay as u32; + log::info!("redbear-compositor: DRM mode {}x{}", width, height); + + let mut crtc_id = 1u32; + if conn.encoder_id != 0 { + let mut encoder_req: DrmGetEncoder = unsafe { std::mem::zeroed() }; + encoder_req.encoder_id = conn.encoder_id; + let mut encoder_req_buf = vec![0u8; std::mem::size_of::()]; + unsafe { + std::ptr::copy_nonoverlapping( + &encoder_req as *const DrmGetEncoder as *const u8, + encoder_req_buf.as_mut_ptr(), + encoder_req_buf.len(), + ); + } + let mut encoder_resp = vec![0u8; std::mem::size_of::()]; + if drm_ioctl( + &mut file, + DRM_IOCTL_MODE_GETCONNECTOR - 1, + &encoder_req_buf, + &mut encoder_resp, + ) + .is_ok() + { + let encoder = unsafe { *(encoder_resp.as_ptr() as *const DrmGetEncoder) }; + if encoder.crtc_id != 0 { + crtc_id = encoder.crtc_id; + } + } + } + + // 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::()]; + 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::()]; + 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::(), + ); + } + 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::()]; + 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::()]; + if drm_ioctl(&mut file, DRM_IOCTL_MODE_MAP_DUMB, &map_req, &mut map_resp).is_err() { + return None; + } + unsafe { + std::ptr::copy_nonoverlapping( + map_resp.as_ptr(), + &mut map as *mut DrmMapDumb as *mut u8, + std::mem::size_of::(), + ); + } + let buf_size = dumb.size as usize; + if map.offset == 0 { + return None; + } + buffers.push((map.offset as usize, 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::()]; + 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::()]; + 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::(), + ); + } + 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 = crtc_id; + setcrtc.fb_handle = fb_ids[0]; + setcrtc.connector_count = 1; + setcrtc.connectors[0] = connector_id; + setcrtc.mode = *mode; + let mut setcrtc_req = vec![0u8; std::mem::size_of::()]; + 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; + } + + log::info!( + "redbear-compositor: DRM output {}x{} stride={} connector={} crtc={}", + width, height, stride, connector_id, crtc_id + ); + Some(DrmOutput { + width, + height, + stride, + buffers, + fb_ids, + current: AtomicUsize::new(0), + _file: 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); + } + + pub fn buffer_ptr(&self, idx: usize) -> *mut u8 { + self.buffers[idx].0 as *mut u8 + } + } +} +#[cfg(not(target_os = "redox"))] +pub mod drm_backend { + use std::sync::atomic::AtomicUsize; + + pub struct DrmOutput { + pub width: u32, + pub height: u32, + pub stride: u32, + pub buffers: Vec>, + pub current: AtomicUsize, + } + + impl DrmOutput { + pub fn open() -> Option { + None + } + + pub fn flip(&self) {} + + pub fn buffer_ptr(&self, idx: usize) -> *mut u8 { + self.buffers[idx].as_ptr() as *mut u8 + } + } +} + +fn push_u32(buf: &mut Vec, value: u32) { + buf.extend_from_slice(&value.to_le_bytes()); +} + +fn push_i32(buf: &mut Vec, value: i32) { + buf.extend_from_slice(&value.to_le_bytes()); +} + +fn push_header(buf: &mut Vec, 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) { + while buf.len() % 4 != 0 { + buf.push(0); + } +} + +fn push_wayland_string(buf: &mut Vec, 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 { + 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 { + if *cursor + 4 > data.len() { + return Err(String::from( + "unexpected end of message while reading string length", + )); + } + let length = u32::from_le_bytes([ + data[*cursor], + data[*cursor + 1], + data[*cursor + 2], + data[*cursor + 3], + ]) as usize; + *cursor += 4; + 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 read_payload_string(payload: &[u8]) -> Option<&str> { + if payload.len() < 4 { return None; } + let len = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]) as usize; + if 4 + len > payload.len() { return None; } + let bytes = &payload[4..4 + len]; + let null_pos = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); + std::str::from_utf8(&bytes[..null_pos]).ok() +} + +fn read_payload_i32(payload: &[u8], idx: usize) -> Option { + let off = idx * 4; + if off + 4 > payload.len() { return None; } + Some(i32::from_le_bytes([payload[off], payload[off+1], payload[off+2], payload[off+3]])) +} + +fn read_payload_u32(payload: &[u8], idx: usize) -> Option { + let off = idx * 4; + if off + 4 > payload.len() { return None; } + Some(u32::from_le_bytes([payload[off], payload[off+1], payload[off+2], payload[off+3]])) +} + +fn recv_with_rights( + stream: &mut UnixStream, + data: &mut [u8], +) -> std::io::Result<(usize, VecDeque)> { + 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::()); + let fd_count = data_len / mem::size_of::(); + let data_ptr = unsafe { libc::CMSG_DATA(cmsg).cast::() }; + 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)) +} + +fn send_with_rights( + stream: &mut UnixStream, + object_id: u32, + opcode: u16, + payload: &[u8], + fds: &[RawFd], +) -> std::io::Result<()> { + let size = 8 + payload.len(); + let mut msg = Vec::with_capacity(size); + push_u32(&mut msg, object_id); + push_u32(&mut msg, ((size as u32) << 16) | u32::from(opcode)); + msg.extend_from_slice(payload); + + if fds.is_empty() { + stream.write_all(&msg)?; + return Ok(()); + } + + let mut iov = libc::iovec { + iov_base: msg.as_mut_ptr().cast(), + iov_len: msg.len(), + }; + let control_len = unsafe { libc::CMSG_SPACE((fds.len() * mem::size_of::()) as u32) as usize }; + let mut control = vec![0u8; control_len]; + let 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, + }; + + unsafe { + let cmsg = libc::CMSG_FIRSTHDR(&header); + if cmsg.is_null() { + return Err(std::io::Error::other("failed to allocate SCM_RIGHTS header")); + } + (*cmsg).cmsg_level = libc::SOL_SOCKET; + (*cmsg).cmsg_type = libc::SCM_RIGHTS; + (*cmsg).cmsg_len = libc::CMSG_LEN((fds.len() * mem::size_of::()) as u32) as _; + std::ptr::copy_nonoverlapping( + fds.as_ptr().cast::(), + libc::CMSG_DATA(cmsg).cast::(), + fds.len() * mem::size_of::(), + ); + } + + let written = unsafe { libc::sendmsg(stream.as_raw_fd(), &header, 0) }; + if written < 0 { + return Err(std::io::Error::last_os_error()); + } + if written as usize != msg.len() { + return Err(std::io::Error::other(format!( + "short sendmsg write: expected {}, got {}", + msg.len(), + written + ))); + } + + Ok(()) +} + +fn send_with_rights_fds( + stream: &mut UnixStream, + msg: &[u8], + fds: &[RawFd], +) -> std::io::Result<()> { + if fds.is_empty() { + stream.write_all(msg)?; + return Ok(()); + } + + let mut msg = msg.to_vec(); + let mut iov = libc::iovec { + iov_base: msg.as_mut_ptr().cast(), + iov_len: msg.len(), + }; + let control_len = unsafe { libc::CMSG_SPACE((fds.len() * mem::size_of::()) as u32) as usize }; + let mut control = vec![0u8; control_len]; + let 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 cmsg = unsafe { libc::CMSG_FIRSTHDR(&header) }; + if !cmsg.is_null() { + unsafe { + (*cmsg).cmsg_level = libc::SOL_SOCKET; + (*cmsg).cmsg_type = libc::SCM_RIGHTS; + (*cmsg).cmsg_len = unsafe { + libc::CMSG_LEN((fds.len() * mem::size_of::()) as u32) + } as _; + std::ptr::copy_nonoverlapping( + fds.as_ptr(), + libc::CMSG_DATA(cmsg).cast::(), + fds.len(), + ); + } + } + + let written = unsafe { libc::sendmsg(stream.as_raw_fd(), &header, 0) }; + if written < 0 { + return Err(std::io::Error::last_os_error()); + } + if written as usize != msg.len() { + return Err(std::io::Error::other(format!( + "short sendmsg write: expected {}, got {}", + msg.len(), + written + ))); + } + + Ok(()) +} + +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_REGISTRY_GLOBAL_REMOVE: u16 = 1; + +const WL_FIXES_DESTROY: u16 = 0; +const WL_FIXES_DESTROY_REGISTRY: u16 = 1; +const WL_FIXES_ACK_GLOBAL_REMOVE: u16 = 2; + +const WL_COMPOSITOR_CREATE_SURFACE: u16 = 0; +const WL_COMPOSITOR_CREATE_REGION: u16 = 1; + +const WL_SHM_CREATE_POOL: u16 = 0; +const WL_SHM_RELEASE: u16 = 1; +const WL_SHM_FORMAT: u16 = 0; + +const WL_SHM_POOL_CREATE_BUFFER: u16 = 0; +const WL_SHM_POOL_DESTROY: u16 = 1; +const WL_SHM_POOL_RESIZE: u16 = 2; + +const WL_BUFFER_DESTROY: u16 = 0; +const WL_BUFFER_RELEASE: u16 = 0; + +const WL_SURFACE_DESTROY: u16 = 0; +const WL_SURFACE_ATTACH: u16 = 1; +const WL_SURFACE_DAMAGE: u16 = 2; +const WL_SURFACE_FRAME: u16 = 3; +const WL_SURFACE_SET_OPAQUE_REGION: u16 = 4; +const WL_SURFACE_SET_INPUT_REGION: u16 = 5; +const WL_SURFACE_COMMIT: u16 = 6; +const WL_REGION_DESTROY: u16 = 0; +const WL_REGION_ADD: u16 = 1; +const WL_REGION_SUBTRACT: u16 = 2; + +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_CREATE_POSITIONER: u16 = 1; +const XDG_WM_BASE_GET_XDG_SURFACE: u16 = 2; +const XDG_WM_BASE_PONG: u16 = 3; +const XDG_WM_BASE_PING: u16 = 0; + +const XDG_SURFACE_DESTROY: u16 = 0; +const XDG_SURFACE_GET_TOPLEVEL: u16 = 1; +const XDG_SURFACE_GET_POPUP: u16 = 2; +const XDG_SURFACE_SET_WINDOW_GEOMETRY: u16 = 3; +const XDG_SURFACE_ACK_CONFIGURE: u16 = 4; +const XDG_SURFACE_CONFIGURE: u16 = 0; + +const XDG_TOPLEVEL_CONFIGURE: u16 = 0; +const XDG_TOPLEVEL_CLOSE: u16 = 1; +const XDG_TOPLEVEL_CONFIGURE_BOUNDS: u16 = 2; +const XDG_TOPLEVEL_WM_CAPABILITIES: u16 = 3; +const XDG_TOPLEVEL_DESTROY: u16 = 0; +const XDG_TOPLEVEL_SET_PARENT: u16 = 1; +const XDG_TOPLEVEL_SET_TITLE: u16 = 2; +const XDG_TOPLEVEL_SET_APP_ID: u16 = 3; +const XDG_TOPLEVEL_SHOW_WINDOW_MENU: u16 = 4; +const XDG_TOPLEVEL_MOVE: u16 = 5; +const XDG_TOPLEVEL_RESIZE: u16 = 6; +const XDG_TOPLEVEL_SET_MAX_SIZE: u16 = 7; +const XDG_TOPLEVEL_SET_MIN_SIZE: u16 = 8; +const XDG_TOPLEVEL_SET_MAXIMIZED: u16 = 9; +const XDG_TOPLEVEL_UNSET_MAXIMIZED: u16 = 10; +const XDG_TOPLEVEL_SET_FULLSCREEN: u16 = 11; +const XDG_TOPLEVEL_UNSET_FULLSCREEN: u16 = 12; +const XDG_TOPLEVEL_SET_MINIMIZED: u16 = 13; + +const XDG_POSITIONER_DESTROY: u16 = 0; +const XDG_POSITIONER_SET_SIZE: u16 = 1; +const XDG_POSITIONER_SET_ANCHOR_RECT: u16 = 2; +const XDG_POSITIONER_SET_ANCHOR: u16 = 3; +const XDG_POSITIONER_SET_GRAVITY: u16 = 4; +const XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT: u16 = 5; +const XDG_POSITIONER_SET_OFFSET: u16 = 6; +const XDG_POSITIONER_SET_REACTIVE: u16 = 7; +const XDG_POSITIONER_SET_PARENT_SIZE: u16 = 8; +const XDG_POSITIONER_SET_PARENT_CONFIGURE: u16 = 9; + +const XDG_POPUP_DESTROY: u16 = 0; +const XDG_POPUP_GRAB: u16 = 1; +const XDG_POPUP_REPOSITION: u16 = 2; +const XDG_POPUP_CONFIGURE: u16 = 0; +const XDG_POPUP_POPUP_DONE: u16 = 1; +const XDG_POPUP_REPOSITIONED: u16 = 2; + +const WL_SEAT_GET_POINTER: u16 = 0; +const WL_SEAT_GET_KEYBOARD: u16 = 1; +const WL_SEAT_GET_TOUCH: u16 = 2; +const WL_SEAT_RELEASE: u16 = 3; +const WL_SEAT_CAPABILITIES: u16 = 0; +const WL_SEAT_NAME: u16 = 1; + +const WL_POINTER_RELEASE: u16 = 0; +const WL_POINTER_SET_CURSOR: u16 = 1; +const WL_POINTER_BUTTON: u16 = 4; +const WL_KEYBOARD_RELEASE: u16 = 0; +const WL_TOUCH_RELEASE: u16 = 0; +const WL_KEYBOARD_MODIFIERS: u16 = 4; +const WL_KEYBOARD_REPEAT_INFO: u16 = 5; + +const WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP: u32 = 0; + +const WL_DATA_DEVICE_MANAGER_CREATE_DATA_SOURCE: u16 = 0; +const WL_DATA_DEVICE_MANAGER_GET_DATA_DEVICE: u16 = 1; +const WL_DATA_SOURCE_OFFER: u16 = 0; +const WL_DATA_SOURCE_DESTROY: u16 = 1; +const WL_DATA_SOURCE_SET_ACTIONS: u16 = 2; +const WL_DATA_DEVICE_START_DRAG: u16 = 0; +const WL_DATA_DEVICE_SET_SELECTION: u16 = 1; +const WL_DATA_DEVICE_RELEASE: u16 = 2; + +// 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_OUTPUT_RELEASE: u16 = 0; +const WL_OUTPUT_NAME: u16 = 4; +const WL_OUTPUT_DESCRIPTION: u16 = 5; + +const WL_CALLBACK_DONE: u16 = 0; + +const WL_SHM_FORMAT_XRGB8888: u32 = 1; +const WL_SHM_FORMAT_ARGB8888: u32 = 0; +const DRM_FORMAT_XRGB8888: u32 = 0x34325258; +const DRM_FORMAT_ARGB8888: u32 = 0x34325241; + +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; +const OBJECT_TYPE_WL_DATA_DEVICE: u32 = 19; +const OBJECT_TYPE_WL_SUBSURFACE: u32 = 20; +const OBJECT_TYPE_WL_FIXES: u32 = 21; +const OBJECT_TYPE_WL_REGION: u32 = 22; +const OBJECT_TYPE_WL_TOUCH: u32 = 23; +const OBJECT_TYPE_WL_DATA_SOURCE: u32 = 24; +const OBJECT_TYPE_XDG_POSITIONER: u32 = 25; +const OBJECT_TYPE_XDG_POPUP: u32 = 26; +const OBJECT_TYPE_ZXDG_DECORATION_MANAGER_V1: u32 = 27; +const OBJECT_TYPE_ZXDG_TOPLEVEL_DECORATION_V1: u32 = 28; +const OBJECT_TYPE_WP_VIEWPORTER: u32 = 29; +const OBJECT_TYPE_WP_VIEWPORT: u32 = 30; +const OBJECT_TYPE_ZWP_LINUX_DMABUF_V1: u32 = 31; +const OBJECT_TYPE_ZWP_LINUX_BUFFER_PARAMS_V1: u32 = 32; +const OBJECT_TYPE_WP_PRESENTATION: u32 = 33; +const OBJECT_TYPE_WP_PRESENTATION_FEEDBACK: u32 = 34; +const OBJECT_TYPE_ZWLR_LAYER_SHELL_V1: u32 = 35; +const OBJECT_TYPE_ZWLR_LAYER_SURFACE_V1: u32 = 36; +const OBJECT_TYPE_ZWLR_OUTPUT_MANAGER_V1: u32 = 37; +const OBJECT_TYPE_ZWLR_OUTPUT_CONFIG_V1: u32 = 38; +const OBJECT_TYPE_WL_DATA_OFFER: u32 = 35; +const WL_DATA_DEVICE_DATA_OFFER: u16 = 0; +const WL_DATA_DEVICE_SELECTION: u16 = 5; +const WL_DATA_OFFER_ACCEPT: u16 = 0; +const WL_DATA_OFFER_DESTROY: u16 = 2; +const WL_DATA_OFFER_FINISH: u16 = 3; +const WL_DATA_OFFER_OFFER: u16 = 0; +const WL_DATA_OFFER_RECEIVE: u16 = 1; +const WL_DATA_OFFER_SOURCE_ACTIONS: u16 = 1; + +const ZWLR_LAYER_SURFACE_V1_CONFIGURE_EVENT: u16 = 0; +const ZWLR_OUTPUT_CONFIG_HEAD_V1_EVENT: u16 = 0; +const ZWLR_OUTPUT_CONFIG_V1_SUCCEEDED_EVENT: u16 = 0; + +// wl_subcompositor opcodes +const WL_SUBCOMPOSITOR_GET_SUBSURFACE: u16 = 1; +const WL_SUBCOMPOSITOR_DESTROY: u16 = 0; +const WL_SUBSURFACE_DESTROY: u16 = 0; +const WL_SUBSURFACE_SET_POSITION: u16 = 1; +const WL_SUBSURFACE_PLACE_ABOVE: u16 = 2; +const WL_SUBSURFACE_PLACE_BELOW: u16 = 3; +const WL_SUBSURFACE_SET_SYNC: u16 = 4; +const WL_SUBSURFACE_SET_DESYNC: u16 = 5; + +const ZXDG_DECORATION_MANAGER_V1_DESTROY: u16 = 0; +const ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION: u16 = 1; +const ZXDG_TOPLEVEL_DECORATION_V1_DESTROY: u16 = 0; +const ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE: u16 = 1; +const ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE: u16 = 2; +const ZXDG_TOPLEVEL_DECORATION_V1_CONFIGURE: u16 = 0; +const ZXDG_TOPLEVEL_DECORATION_MODE_SERVER_SIDE: u32 = 2; + +const WP_VIEWPORTER_DESTROY: u16 = 0; +const WP_VIEWPORTER_GET_VIEWPORT: u16 = 1; +const WP_VIEWPORT_DESTROY: u16 = 0; +const WP_VIEWPORT_SET_SOURCE: u16 = 1; +const WP_VIEWPORT_SET_DESTINATION: u16 = 2; + +const ZWP_LINUX_DMABUF_V1_DESTROY: u16 = 0; +const ZWP_LINUX_DMABUF_V1_CREATE_PARAMS: u16 = 1; +const ZWP_LINUX_DMABUF_V1_FORMAT: u16 = 1; +const ZWP_LINUX_DMABUF_V1_MODIFIER: u16 = 0; +const ZWP_LINUX_BUFFER_PARAMS_V1_DESTROY: u16 = 0; +const ZWP_LINUX_BUFFER_PARAMS_V1_ADD: u16 = 1; +const ZWP_LINUX_BUFFER_PARAMS_V1_CREATE: u16 = 2; +const ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_IMMED: u16 = 3; + +const WP_PRESENTATION_DESTROY: u16 = 0; +const WP_PRESENTATION_FEEDBACK: u16 = 1; +const WP_PRESENTATION_FEEDBACK_DESTROY: u16 = 0; +const WP_PRESENTATION_FEEDBACK_DISCARDED: u16 = 1; +const WP_PRESENTATION_FEEDBACK_PRESENTED: u16 = 0; + +const ZWLR_LAYER_SHELL_V1_DESTROY: u16 = 0; +const ZWLR_LAYER_SHELL_V1_GET_LAYER_SURFACE: u16 = 1; +const ZWLR_LAYER_SURFACE_V1_DESTROY: u16 = 0; +const ZWLR_LAYER_SURFACE_V1_ACK_CONFIGURE: u16 = 7; +const ZWLR_OUTPUT_MANAGER_V1_DESTROY: u16 = 0; +const ZWLR_OUTPUT_MANAGER_V1_CREATE_CONFIGURATION: u16 = 1; +const ZWLR_OUTPUT_CONFIG_V1_DESTROY: u16 = 0; +const ZWLR_OUTPUT_CONFIG_V1_APPLY: u16 = 3; +const ZWLR_OUTPUT_CONFIG_V1_TEST: u16 = 4; + +pub struct Global { + pub name: u32, + pub interface: String, + pub version: u32, +} + +pub struct ShmPool { + file: std::fs::File, + size: usize, +} + +#[derive(Clone)] +pub struct Buffer { + pub pool_id: u32, + pub offset: u32, + pub width: u32, + pub height: u32, + pub stride: u32, + _format: u32, +} + +#[derive(Clone)] +pub struct Surface { + pub buffer: Option, + pub pending_buffer_id: Option, + pub committed_buffer_id: Option, + pub x: u32, + pub y: u32, + _width: u32, + _height: u32, + pub geometry: Option, + pub role: Option, + pub mapped: bool, +} + +#[derive(Clone, Copy)] +pub struct WindowGeometry { x: i32, y: i32, width: i32, height: i32 } + +#[derive(Clone, Default)] +pub struct ConfigureState { + pending_serial: Option, + last_acked_serial: Option, + configured: bool, +} + +impl ConfigureState { + fn ack(&mut self, serial: u32) { + self.last_acked_serial = Some(serial); + if self.pending_serial == Some(serial) { self.configured = true; } + } + fn can_present(&self) -> bool { self.pending_serial.is_none() || self.configured } +} + +#[derive(Clone, Default)] +pub struct ToplevelState { + pub object_id: u32, + pub parent_id: Option, + pub title: Option, + pub app_id: Option, + pub min_size: Option<(i32, i32)>, + pub max_size: Option<(i32, i32)>, + pub maximized: bool, + pub fullscreen: bool, + pub minimized: bool, + pub configure: ConfigureState, + pub last_move_serial: Option, + pub last_resize_serial: Option, + pub last_window_menu_serial: Option, +} + +#[derive(Clone, Default)] +pub struct PopupState { + pub object_id: u32, + pub parent_id: Option, + pub positioner_id: Option, + pub grab_serial: Option, + pub configure: ConfigureState, +} + +#[derive(Clone, Default)] +pub struct ShellSurfaceState { + pub object_id: u32, + pub surface_id: u32, + pub kind: ShellSurfaceKind, + pub title: Option, + pub class: Option, + pub parent_surface_id: Option, + pub popup_serial: Option, + pub last_ping_serial: Option, +} + +#[derive(Clone, Copy, Default)] +pub enum ShellSurfaceKind { #[default] None, Toplevel, Popup, Transient, Fullscreen, Maximized } + +#[derive(Clone)] +pub enum SurfaceRole { + Toplevel(ToplevelState), + Popup(PopupState), + Shell(ShellSurfaceState), +} + +impl SurfaceRole { + fn ack_configure(&mut self, serial: u32) { + match self { + SurfaceRole::Toplevel(s) => s.configure.ack(serial), + SurfaceRole::Popup(s) => s.configure.ack(serial), + SurfaceRole::Shell(_) => {} + } + } +} + +#[derive(Clone, Default)] +pub struct PositionerState { + size: Option<(i32, i32)>, + anchor_rect: Option<(i32, i32, i32, i32)>, + anchor: Option, + gravity: Option, + constraint_adjustment: Option, + offset: Option<(i32, i32)>, + reactive: Option, + parent_size: Option<(i32, i32)>, + parent_configure: Option, +} + +#[derive(Clone, Default)] +pub struct DataSourceState { + pub mime_types: Vec, + pub actions: Option, + pub buffer: Option>, +} + +#[derive(Clone, Default)] +pub struct DataDeviceState { + pub selection_source: Option, + pub drag_source: Option, + pub selection_offer: Option, +} + +#[derive(Clone, Default)] +pub struct DataOfferState { + pub source_client_id: u32, + pub source_id: u32, + pub mime_types: Vec, + pub accepted_mime: Option, + pub actions: Option, + pub buffer: Option>, + pub finished: bool, +} + +#[derive(Clone, Default)] +pub struct SubsurfaceState { + pub surface_id: u32, + pub parent_surface_id: u32, + pub x: i32, + pub y: i32, + pub sync: bool, + pub z_index: i32, +} + +#[derive(Clone, Default)] +pub struct PointerState { + pub cursor_surface: Option, + pub cursor_serial: Option, + pub cursor_hotspot: (i32, i32), + pub last_button_serial: Option, + pub last_button: Option, +} + +#[derive(Clone, Default)] +pub struct KeyboardEvent { + keycode: u32, + state: u32, + time: u32, +} + +const MOD_SHIFT: u32 = 1 << 0; +const MOD_CAPS: u32 = 1 << 1; +const MOD_CTRL: u32 = 1 << 2; +const MOD_ALT: u32 = 1 << 3; +const MOD_MOD2: u32 = 1 << 4; +const MOD_MOD3: u32 = 1 << 5; +const MOD_LOGO: u32 = 1 << 6; +const MOD_MOD5: u32 = 1 << 7; + +const KEY_LEFT_SHIFT: u32 = 50; +const KEY_RIGHT_SHIFT: u32 = 62; +const KEY_LEFT_CTRL: u32 = 37; +const KEY_RIGHT_CTRL: u32 = 105; +const KEY_LEFT_ALT: u32 = 64; +const KEY_RIGHT_ALT: u32 = 108; +const KEY_LEFT_LOGO: u32 = 133; +const KEY_RIGHT_LOGO: u32 = 134; +const KEY_CAPS_LOCK: u32 = 66; +const KEY_NUM_LOCK: u32 = 77; +const KEY_MOD5: u32 = 116; + +#[derive(Clone, Default)] +pub struct KeyboardState { + pub focused_surface: Option, + pub pending_events: VecDeque, + pub last_keycode: Option, + pub last_state: Option, + pub last_time: Option, + pub depressed: u32, + pub latched: u32, + pub locked: u32, + pub group: u32, +} + +impl KeyboardState { + fn modifier_bit_for_key(keycode: u32) -> Option { + match keycode { + k if k == KEY_LEFT_SHIFT || k == KEY_RIGHT_SHIFT => Some(MOD_SHIFT), + k if k == KEY_LEFT_CTRL || k == KEY_RIGHT_CTRL => Some(MOD_CTRL), + k if k == KEY_LEFT_ALT || k == KEY_RIGHT_ALT => Some(MOD_ALT), + k if k == KEY_LEFT_LOGO || k == KEY_RIGHT_LOGO => Some(MOD_LOGO), + KEY_CAPS_LOCK => Some(MOD_CAPS), + KEY_NUM_LOCK => Some(MOD_MOD2), + KEY_MOD5 => Some(MOD_MOD5), + _ => None, + } + } + + fn apply_modifier_event(&mut self, keycode: u32, pressed: bool) { + let Some(bit) = Self::modifier_bit_for_key(keycode) else { + return; + }; + if keycode == KEY_CAPS_LOCK || keycode == KEY_NUM_LOCK { + if pressed { + self.locked ^= bit; + } + return; + } + if pressed { + self.depressed |= bit; + } else { + self.depressed &= !bit; + } + } +} + +#[derive(Clone, Default)] +pub struct InteractiveGrabState { + object_id: Option, + seat_id: Option, + surface_id: Option, + serial: Option, + kind: Option, +} + +pub struct ClientState { + pub objects: HashMap, + pub object_versions: HashMap, + pub surfaces: HashMap, + pub buffers: HashMap, + pub shm_pools: HashMap, + pub positioners: HashMap, + pub shell_surfaces: HashMap, + pub subsurfaces: HashMap, + pub data_sources: HashMap, + pub data_devices: HashMap, + pub data_offers: HashMap, + pub xdg_to_surface: HashMap, + pub decorations: HashMap, + pub dmabuf_params: HashMap, + pub viewporters: HashMap, + pub presentation_feedback: HashMap, + pub regions: HashMap, + pub acked_global_removals: HashSet, + pub _next_id: u32, +} + +#[derive(Clone, Default)] +pub struct DmabufParamsState { + planes: Vec, + width: Option, + height: Option, + format: Option, + modifier: Option<(u32, u32)>, + flags: Option, + created: bool, +} + +#[derive(Clone, Default)] +pub struct DmabufPlane { + fd: Option, + plane_idx: Option, + offset: Option, + stride: Option, + modifier_hi: Option, + modifier_lo: Option, +} + +#[derive(Clone, Default)] +pub struct ViewportState { + source: Option<(i32, i32, i32, i32)>, + destination: Option<(i32, i32)>, +} + +#[derive(Clone, Default)] +pub struct PresentationFeedbackState { + surface_id: Option, + last_feedback_serial: Option, +} + +/// Wayland `wl_region` payload: x, y, width, height in surface-local coords. +#[derive(Clone, Copy, Default, Debug)] +pub struct Rect { + x: i32, + y: i32, + w: i32, + h: i32, +} + +/// wl_region add/subtract rectangle lists. The effective region is the union +/// of `added` minus the union of `subtracted` (Wayland protocol semantics). +#[derive(Clone, Default)] +pub struct RegionState { + added: Vec, + subtracted: Vec, +} + +impl RegionState { + /// Point-in-region test: inside any added rect AND not inside any subtracted rect. + fn contains_point(&self, px: i32, py: i32) -> bool { + let in_added = self.added.iter().any(|r| { + px >= r.x && px < r.x + r.w && py >= r.y && py < r.y + r.h + }); + if !in_added { + return false; + } + !self.subtracted.iter().any(|r| { + px >= r.x && px < r.x + r.w && py >= r.y && py < r.y + r.h + }) + } + + fn bounding_box(&self) -> Option { + let mut min_x = i32::MAX; + let mut min_y = i32::MAX; + let mut max_x = i32::MIN; + let mut max_y = i32::MIN; + for r in self.added.iter().filter(|r| r.w > 0 && r.h > 0) { + min_x = min_x.min(r.x); + min_y = min_y.min(r.y); + max_x = max_x.max(r.x + r.w); + max_y = max_y.max(r.y + r.h); + } + if max_x <= min_x || max_y <= min_y { + return None; + } + Some(Rect { x: min_x, y: min_y, w: max_x - min_x, h: max_y - min_y }) + } + + fn is_empty(&self) -> bool { + self.added.iter().all(|r| r.w <= 0 || r.h <= 0) + } +} + +pub struct Compositor { + pub listener: UnixListener, + pub next_id: AtomicU32, + pub next_serial: AtomicU32, + pub globals: Vec, + pub fb_width: u32, + pub fb_height: u32, + pub fb_stride: u32, + pub fb_data: Mutex, + pub drm: Mutex>, + pub clients: Mutex>, + pub pointer_state: Mutex, + pub keyboard_state: Mutex, + pub interactive_grab: Mutex, + pub refresh_nsec: u64, + pub frame_seq: AtomicU64, + pub pending_feedbacks: Mutex>, +} + +pub struct PendingFeedback { + pub client_id: u32, + pub feedback_id: u32, + pub surface_id: u32, + pub queue_time_nsec: u64, +} + +impl Compositor { + pub fn new( + socket_path: &str, + fb_phys: usize, + fb_width: u32, + fb_height: u32, + fb_stride: u32, + drm: Mutex>, + ) -> std::io::Result { + // Ensure XDG_RUNTIME_DIR exists before binding the Wayland socket. On + // Redox there is no logind/pam_systemd to create it, so without this the + // bind fails with ENOENT (e.g. /tmp/run/redbear-greeter never created by + // the gated-off greeterd) and the SDDM greeter never gets a compositor. + let runtime_dir = std::path::Path::new(socket_path) + .parent() + .unwrap_or(std::path::Path::new("/tmp")) + .to_path_buf(); + let _ = std::fs::create_dir_all(&runtime_dir); + let _ = std::fs::remove_file(socket_path); + let listener = UnixListener::bind(socket_path)?; + 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: 2, + }, + 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: 4, + }, + Global { + name: 6, + interface: "xdg_wm_base".into(), + version: 1, + }, + Global { + name: 7, + interface: "wl_fixes".into(), + version: 2, + }, + Global { + name: 8, + interface: "wl_data_device_manager".into(), + version: 3, + }, + Global { + name: 9, + interface: "wl_subcompositor".into(), + version: 1, + }, + Global { + name: 10, + interface: "zxdg_decoration_manager_v1".into(), + version: 1, + }, + Global { + name: 11, + interface: "zwp_linux_dmabuf_v1".into(), + version: 3, + }, + Global { + name: 12, + interface: "wp_viewporter".into(), + version: 1, + }, + Global { + name: 13, + interface: "wp_presentation".into(), + version: 1, + }, + Global { + name: 14, + interface: "zwlr_layer_shell_v1".into(), + version: 4, + }, + Global { + name: 15, + interface: "zwlr_output_manager_v1".into(), + version: 4, + }, + ]; + + 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()), + pointer_state: Mutex::new(PointerState::default()), + keyboard_state: Mutex::new(KeyboardState::default()), + interactive_grab: Mutex::new(InteractiveGrabState::default()), + refresh_nsec: 16_693_334, + frame_seq: AtomicU64::new(0), + pending_feedbacks: Mutex::new(Vec::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) + } + + fn with_toplevel_state_mut( + &self, client_id: u32, toplevel_id: u32, f: F, + ) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + for surface in client.surfaces.values_mut() { + if let Some(SurfaceRole::Toplevel(ref mut ts)) = surface.role { + if ts.object_id == toplevel_id { + f(ts); + return; + } + } + } + } + } + + pub fn stack_surface_relative( + client: &mut ClientState, + surface_id: u32, + sibling_surface_id: u32, + place_above: bool, + ) { + let sibling_z = client + .subsurfaces + .values() + .find(|ss| ss.surface_id == sibling_surface_id) + .map(|ss| ss.z_index) + .unwrap_or(0); + + if let Some(subsurface) = client + .subsurfaces + .values_mut() + .find(|ss| ss.surface_id == surface_id) + { + subsurface.z_index = if place_above { sibling_z + 1 } else { sibling_z - 1 }; + } + } + + pub fn disconnect_client(&self, client_id: u32, stream: &mut UnixStream, reason: &str) { + log::info!("redbear-compositor: disconnecting client {}: {}", client_id, reason); + let _ = stream.shutdown(Shutdown::Both); + self.clients.lock().expect("clients lock").remove(&client_id); + } + + pub fn write_event( + &self, + client_id: u32, + stream: &mut UnixStream, + msg: &[u8], + context: &str, + ) -> Result<(), String> { + match stream.write_all(msg) { + Ok(()) => Ok(()), + Err(err) => { + self.disconnect_client( + client_id, + stream, + &format!("{} write failed: {}", context, err), + ); + Err(format!("{} write failed: {}", context, err)) + } + } + } + + pub fn write_event_with_fds( + &self, + client_id: u32, + stream: &mut UnixStream, + msg: &[u8], + fds: &mut VecDeque, + context: &str, + ) -> Result<(), String> { + if fds.is_empty() { + return self.write_event(client_id, stream, msg, context); + } + let fds_to_send: Vec = fds.drain(..).collect(); + let result = send_with_rights_fds(stream, msg, &fds_to_send); + match result { + Ok(_) => Ok(()), + Err(err) => { + self.disconnect_client( + client_id, + stream, + &format!("{} write failed: {}", context, err), + ); + Err(format!("{} write failed: {}", context, err)) + } + } + } + + pub fn open_pipe_for_payload(&self, bytes: &[u8]) -> Option { + let mut fds = [0i32; 2]; + let rc = unsafe { libc::pipe(fds.as_mut_ptr()) }; + if rc != 0 { + return None; + } + let write_fd = fds[1]; + let close_on_exec_flag = 0o2000000; + unsafe { + libc::fcntl(write_fd, 2, close_on_exec_flag); + } + let result = unsafe { + libc::write( + write_fd, + bytes.as_ptr() as *const _, + bytes.len(), + ) + }; + unsafe { + libc::close(write_fd); + } + if result < 0 { + unsafe { + libc::close(fds[0]); + } + return None; + } + Some(fds[0]) + } + + fn protocol_error( + &self, + client_id: u32, + stream: &mut UnixStream, + interface: &str, + object_id: u32, + opcode: u16, + ) -> Result { + let reason = format!( + "unexpected opcode {} on {} object {}", + opcode, interface, object_id + ); + self.disconnect_client(client_id, stream, &reason); + Err(reason) + } + + pub fn run(&mut self) -> std::io::Result<()> { + log::info!("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(); + log::info!("redbear-compositor: client {} connected", client_id); + self.clients.lock().expect("clients lock").insert( + client_id, + ClientState { + objects: HashMap::new(), + object_versions: HashMap::new(), + surfaces: HashMap::new(), + buffers: HashMap::new(), + shm_pools: HashMap::new(), + positioners: HashMap::new(), + shell_surfaces: HashMap::new(), + subsurfaces: HashMap::new(), + data_sources: HashMap::new(), + data_devices: HashMap::new(), + data_offers: HashMap::new(), + xdg_to_surface: HashMap::new(), + decorations: HashMap::new(), + dmabuf_params: HashMap::new(), + viewporters: HashMap::new(), + presentation_feedback: HashMap::new(), + regions: HashMap::new(), + acked_global_removals: HashSet::new(), + _next_id: 1, + }, + ); + self.handle_client(client_id, stream); + } + Err(e) => log::error!("redbear-compositor: accept error: {}", e), + } + } + Ok(()) + } + + pub fn send_globals( + &self, + client_id: u32, + stream: &mut UnixStream, + registry_id: u32, + ) -> Result<(), String> { + 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); + self.write_event(client_id, stream, &msg, "wl_registry.global")?; + } + Ok(()) + } + + pub fn send_callback_done( + &self, + client_id: u32, + stream: &mut UnixStream, + callback_id: u32, + callback_data: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(12); + push_header(&mut msg, callback_id, WL_CALLBACK_DONE, 4); + push_u32(&mut msg, callback_data); + self.write_event(client_id, stream, &msg, "wl_callback.done") + } + + pub fn send_delete_id( + &self, + client_id: u32, + stream: &mut UnixStream, + deleted_id: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(12); + push_header(&mut msg, 1, WL_DISPLAY_DELETE_ID, 4); + push_u32(&mut msg, deleted_id); + self.write_event(client_id, stream, &msg, "wl_display.delete_id") + } + + #[allow(dead_code)] + pub fn send_global_remove( + &self, + client_id: u32, + stream: &mut UnixStream, + registry_id: u32, + name: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(12); + push_header(&mut msg, registry_id, WL_REGISTRY_GLOBAL_REMOVE, 4); + push_u32(&mut msg, name); + self.write_event(client_id, stream, &msg, "wl_registry.global_remove") + } + + pub 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, _)) => { + log::info!("redbear-compositor: client {} disconnected", client_id); + self.clients.lock().expect("clients lock").remove(&client_id); + break; + } + Ok((n, mut fds)) => { + if let Err(e) = self.dispatch(client_id, &buf[..n], &mut fds, &mut stream) { + log::error!("redbear-compositor: dispatch error: {}", e); + } + while let Some(fd) = fds.pop_front() { + let _ = unsafe { libc::close(fd) }; + } + let _ = self.drain_pending_feedbacks(client_id, &mut stream); + } + Err(e) => { + log::error!("redbear-compositor: read error: {}", e); + break; + } + } + } + self.clients.lock().expect("clients lock").remove(&client_id); + } + + fn dispatch( + &self, + client_id: u32, + data: &[u8], + fds: &mut VecDeque, + 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() + .expect("clients lock") + .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(client_id, 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().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&obj_id); + client.object_versions.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().expect("clients lock"); + 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(client_id, stream, registry_id)?; + } + } + } + _ => { + log::info!( + "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 requested_version = read_u32(payload, &mut cursor)?; + let new_id = read_u32(payload, &mut cursor)?; + + log::info!( + "redbear-compositor: client {} binds '{}' -> id {}", + client_id, iface, new_id + ); + + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + let global_version = self + .globals + .iter() + .find(|global| global.name == name && global.interface == iface) + .map(|global| global.version) + .unwrap_or(requested_version); + let object_version = requested_version.min(global_version); + 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, + "wl_fixes" => OBJECT_TYPE_WL_FIXES, + "zxdg_decoration_manager_v1" => OBJECT_TYPE_ZXDG_DECORATION_MANAGER_V1, + "zwp_linux_dmabuf_v1" => OBJECT_TYPE_ZWP_LINUX_DMABUF_V1, + "wp_viewporter" => OBJECT_TYPE_WP_VIEWPORTER, + "wp_presentation" => OBJECT_TYPE_WP_PRESENTATION, + "zwlr_layer_shell_v1" => OBJECT_TYPE_ZWLR_LAYER_SHELL_V1, + "zwlr_output_manager_v1" => OBJECT_TYPE_ZWLR_OUTPUT_MANAGER_V1, + _ => { + log::info!( + "redbear-compositor: unknown global interface '{}' bound to object {}", + iface, new_id + ); + 0 + } + }; + client.objects.insert(new_id, type_id); + client.object_versions.insert(new_id, object_version); + if iface == "wl_shm" { + self.send_shm_format(client_id, stream, new_id, WL_SHM_FORMAT_ARGB8888)?; + self.send_shm_format(client_id, stream, new_id, WL_SHM_FORMAT_XRGB8888)?; + } + if iface == "wl_output" { + self.send_output_info(client_id, stream, new_id, object_version)?; + } + if iface == "wl_seat" { + self.send_seat_capabilities(client_id, stream, new_id, object_version)?; + } + } + } + _ => { + log::info!( + "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().expect("clients lock"); + 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, + pending_buffer_id: None, + committed_buffer_id: None, + x: 0, + y: 0, + _width: self.fb_width, + _height: self.fb_height, + geometry: None, + role: None, + mapped: false, + }, + ); + } + } + } + WL_COMPOSITOR_CREATE_REGION => { + if payload.len() >= 4 { + let region_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(region_id, OBJECT_TYPE_WL_REGION); + client.regions.insert(region_id, RegionState::default()); + } + } + } + _ => { + log::info!( + "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().expect("clients lock"); + 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) }; + } + } + } + WL_SHM_RELEASE => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + _ => { + log::info!( + "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().expect("clients lock"); + 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, + }, + ), + ); + } + } + } + WL_SHM_POOL_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.shm_pools.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + WL_SHM_POOL_RESIZE => { + if payload.len() >= 4 { + let size = i32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + if size > 0 { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + if let Some(pool) = client.shm_pools.get_mut(&object_id) { + pool.size = pool.size.max(size as usize); + } + } + } + } + } + _ => { + log::info!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_SURFACE => match opcode { + WL_SURFACE_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.surfaces.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + 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().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + let attached_buffer = if buffer_id == 0 { + None + } else { + client.buffers.get(&buffer_id).cloned() + }; + if let Some(surface) = client.surfaces.get_mut(&object_id) { + if buffer_id == 0 { + surface.buffer = None; + surface.pending_buffer_id = None; + } else if let Some((pool_id, buffer)) = attached_buffer { + surface.buffer = Some(Buffer { pool_id, ..buffer }); + surface.pending_buffer_id = Some(buffer_id); + } + } + } + } + } + WL_SURFACE_COMMIT => { + let release_id = { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + if let Some(surface) = client.surfaces.get_mut(&object_id) { + let release_buffer = surface.pending_buffer_id; + surface.committed_buffer_id = release_buffer; + 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); + } + } + release_buffer + } else { + None + } + } else { + None + } + }; + + if let Some(buf_id) = release_id { + if buf_id != 0 { + self.send_buffer_release(client_id, stream, buf_id)?; + } + } + } + WL_SURFACE_DAMAGE => { + // No-op — we don't need damage tracking for a single-client greeter. + } + WL_SURFACE_FRAME => { + if payload.len() >= 4 { + let callback_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + self.send_callback_done( + client_id, + stream, + callback_id, + self.next_serial(), + )?; + } + } + WL_SURFACE_SET_OPAQUE_REGION | WL_SURFACE_SET_INPUT_REGION => { + // Region state is tracked as accepted but inert for the greeter's + // single full-screen composition surface. + } + _ => { + log::info!( + "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().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, OBJECT_TYPE_WL_SHELL_SURFACE); + } + } + } + _ => { + log::info!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_SHELL_SURFACE => match opcode { + WL_SHELL_SURFACE_PONG => { + // Client pong — accepted but compositor doesn't currently ping. + } + WL_SHELL_SURFACE_SET_TOPLEVEL => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + let ss = client.shell_surfaces.entry(object_id).or_default(); + ss.object_id = object_id; + ss.kind = ShellSurfaceKind::Toplevel; + // Associate with surface: look up which surface this shell_surface was created for. + // WL_SHELL_GET_SHELL_SURFACE passes shell_surface_id and surface_id as payload. + // We need to track this — use a reverse map or iterate. + // For now, set the surface role on the most recently created unmapped surface. + for (surface_id, surface) in client.surfaces.iter_mut() { + if surface.role.is_none() && !surface.mapped { + surface.role = Some(SurfaceRole::Shell(ShellSurfaceState { + object_id, + surface_id: *surface_id, + kind: ShellSurfaceKind::Toplevel, + ..Default::default() + })); + ss.surface_id = *surface_id; + break; + } + } + } + } + _ => { + log::info!( + "redbear-compositor: unhandled wl_shell_surface opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_SEAT => match opcode { + WL_SEAT_GET_POINTER | WL_SEAT_GET_KEYBOARD | WL_SEAT_GET_TOUCH => { + 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, + WL_SEAT_GET_TOUCH => OBJECT_TYPE_WL_TOUCH, + // The outer match arm already restricts opcode to the three + // seat-get variants above, so this branch is logically + // unreachable. A malformed/malicious client must never crash the + // compositor though, so reject defensively instead of panicking. + _ => { + return Err(format!( + "redbear-compositor: unexpected wl_seat opcode {} on object {}", + opcode, object_id + )) + } + }; + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, object_type); + } + drop(clients); + if opcode == WL_SEAT_GET_KEYBOARD { + self.send_keyboard_setup(client_id, stream, new_id)?; + } + } + } + WL_SEAT_RELEASE => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.object_versions.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + _ => { + log::info!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_XDG_WM_BASE => match opcode { + XDG_WM_BASE_CREATE_POSITIONER => { + 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().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, OBJECT_TYPE_XDG_POSITIONER); + } + } + } + XDG_WM_BASE_GET_XDG_SURFACE => { + if payload.len() >= 8 { + let new_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let surface_id = u32::from_le_bytes([ + payload[4], payload[5], payload[6], payload[7], + ]); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, OBJECT_TYPE_XDG_SURFACE); + client.xdg_to_surface.insert(new_id, surface_id); + } + } + } + XDG_WM_BASE_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + XDG_WM_BASE_PONG => { + // The compositor does not currently send xdg_wm_base.ping, but accepting + // pong keeps clients tolerant if a future watchdog starts doing so. + } + _ => { + log::info!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_XDG_SURFACE => match opcode { + XDG_SURFACE_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, 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().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(toplevel_id, OBJECT_TYPE_XDG_TOPLEVEL); + // Associate this toplevel with the parent xdg_surface's surface + if let Some(&surface_id) = client.xdg_to_surface.get(&object_id) { + if let Some(surface) = client.surfaces.get_mut(&surface_id) { + surface.role = Some(SurfaceRole::Toplevel(ToplevelState { + object_id: toplevel_id, + ..Default::default() + })); + } + } + } + drop(clients); + let serial = self.next_serial(); + self.send_xdg_toplevel_configure(client_id, stream, toplevel_id)?; + self.send_xdg_surface_configure(client_id, stream, object_id, serial)?; + } + } + XDG_SURFACE_GET_POPUP => { + if payload.len() >= 12 { + let popup_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(popup_id, OBJECT_TYPE_XDG_POPUP); + } + drop(clients); + let serial = self.next_serial(); + self.send_xdg_popup_configure(client_id, stream, popup_id)?; + self.send_xdg_surface_configure(client_id, stream, object_id, serial)?; + } + } + XDG_SURFACE_SET_WINDOW_GEOMETRY => { + if let (Some(x), Some(y), Some(w), Some(h)) = ( + read_payload_i32(payload, 0), read_payload_i32(payload, 1), + read_payload_i32(payload, 2), read_payload_i32(payload, 3), + ) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + if let Some(&surface_id) = client.xdg_to_surface.get(&object_id) { + if let Some(surface) = client.surfaces.get_mut(&surface_id) { + surface.geometry = Some(WindowGeometry { x, y, width: w, height: h }); + } + } + } + } + } + XDG_SURFACE_ACK_CONFIGURE => { + if let Some(serial) = read_payload_u32(payload, 0) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + if let Some(&surface_id) = client.xdg_to_surface.get(&object_id) { + if let Some(surface) = client.surfaces.get_mut(&surface_id) { + surface.role.as_mut().map(|r| r.ack_configure(serial)); + } + } + } + } + } + _ => { + log::info!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_OUTPUT => match opcode { + WL_OUTPUT_RELEASE => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.object_versions.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + _ => { + log::info!( + "redbear-compositor: unhandled wl_output opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_BUFFER => match opcode { + WL_BUFFER_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.object_versions.remove(&object_id); + client.buffers.remove(&object_id); + for surface in client.surfaces.values_mut() { + if surface.committed_buffer_id == Some(object_id) { + surface.committed_buffer_id = None; + } + if surface.pending_buffer_id == Some(object_id) { + surface.pending_buffer_id = None; + surface.buffer = None; + } + } + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + _ => { + log::info!( + "redbear-compositor: unhandled wl_buffer opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_XDG_TOPLEVEL => match opcode { + XDG_TOPLEVEL_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.object_versions.remove(&object_id); + // Clear role on the associated surface + for surface in client.surfaces.values_mut() { + if let Some(SurfaceRole::Toplevel(ref ts)) = surface.role { + if ts.object_id == object_id { + surface.role = None; + break; + } + } + } + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + XDG_TOPLEVEL_SET_TITLE => { + if let Some(title) = read_payload_string(payload) { + self.with_toplevel_state_mut(client_id, object_id, |ts| { + ts.title = Some(title.to_string()); + }); + } + } + XDG_TOPLEVEL_SET_APP_ID => { + if let Some(app_id) = read_payload_string(payload) { + self.with_toplevel_state_mut(client_id, object_id, |ts| { + ts.app_id = Some(app_id.to_string()); + }); + } + } + XDG_TOPLEVEL_SET_PARENT => { + if let Some(parent_id) = read_payload_u32(payload, 0) { + self.with_toplevel_state_mut(client_id, object_id, |ts| { + ts.parent_id = if parent_id != 0 { Some(parent_id) } else { None }; + }); + } + } + XDG_TOPLEVEL_SET_MIN_SIZE => { + if let (Some(w), Some(h)) = (read_payload_i32(payload, 0), read_payload_i32(payload, 1)) { + self.with_toplevel_state_mut(client_id, object_id, |ts| { + ts.min_size = if w != 0 || h != 0 { Some((w, h)) } else { None }; + }); + } + } + XDG_TOPLEVEL_SET_MAX_SIZE => { + if let (Some(w), Some(h)) = (read_payload_i32(payload, 0), read_payload_i32(payload, 1)) { + self.with_toplevel_state_mut(client_id, object_id, |ts| { + ts.max_size = if w != 0 || h != 0 { Some((w, h)) } else { None }; + }); + } + } + XDG_TOPLEVEL_SET_MAXIMIZED => { + self.with_toplevel_state_mut(client_id, object_id, |ts| { + ts.maximized = true; + }); + } + XDG_TOPLEVEL_UNSET_MAXIMIZED => { + self.with_toplevel_state_mut(client_id, object_id, |ts| { + ts.maximized = false; + }); + } + XDG_TOPLEVEL_SET_FULLSCREEN => { + self.with_toplevel_state_mut(client_id, object_id, |ts| { + ts.fullscreen = true; + }); + } + XDG_TOPLEVEL_UNSET_FULLSCREEN => { + self.with_toplevel_state_mut(client_id, object_id, |ts| { + ts.fullscreen = false; + }); + } + XDG_TOPLEVEL_SET_MINIMIZED => { + self.with_toplevel_state_mut(client_id, object_id, |ts| { + ts.minimized = true; + }); + } + XDG_TOPLEVEL_SHOW_WINDOW_MENU => { + self.with_toplevel_state_mut(client_id, object_id, |ts| { + ts.last_window_menu_serial = read_payload_u32(payload, 0); + }); + // Accepted for protocol compliance. Pointer-grab wiring is not yet present. + } + XDG_TOPLEVEL_MOVE => { + self.with_toplevel_state_mut(client_id, object_id, |ts| { + ts.last_move_serial = read_payload_u32(payload, 0); + }); + self.send_xdg_toplevel_configure(client_id, stream, object_id)?; + } + XDG_TOPLEVEL_RESIZE => { + self.with_toplevel_state_mut(client_id, object_id, |ts| { + ts.last_resize_serial = read_payload_u32(payload, 0); + }); + self.send_xdg_toplevel_configure(client_id, stream, object_id)?; + } + _ => { + log::info!( + "redbear-compositor: unhandled xdg_toplevel opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_XDG_POSITIONER => match opcode { + XDG_POSITIONER_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.object_versions.remove(&object_id); + client.positioners.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + XDG_POSITIONER_SET_SIZE => { + if let (Some(w), Some(h)) = (read_payload_i32(payload, 0), read_payload_i32(payload, 1)) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.positioners.entry(object_id).or_default().size = Some((w, h)); + } + } + } + XDG_POSITIONER_SET_ANCHOR_RECT => { + if let (Some(x), Some(y), Some(w), Some(h)) = ( + read_payload_i32(payload, 0), read_payload_i32(payload, 1), + read_payload_i32(payload, 2), read_payload_i32(payload, 3), + ) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.positioners.entry(object_id).or_default().anchor_rect = Some((x, y, w, h)); + } + } + } + XDG_POSITIONER_SET_ANCHOR => { + if let Some(anchor) = read_payload_u32(payload, 0) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.positioners.entry(object_id).or_default().anchor = Some(anchor); + } + } + } + XDG_POSITIONER_SET_GRAVITY => { + if let Some(gravity) = read_payload_u32(payload, 0) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.positioners.entry(object_id).or_default().gravity = Some(gravity); + } + } + } + XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT => { + if let Some(adj) = read_payload_u32(payload, 0) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.positioners.entry(object_id).or_default().constraint_adjustment = Some(adj); + } + } + } + XDG_POSITIONER_SET_OFFSET => { + if let (Some(x), Some(y)) = (read_payload_i32(payload, 0), read_payload_i32(payload, 1)) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.positioners.entry(object_id).or_default().offset = Some((x, y)); + } + } + } + XDG_POSITIONER_SET_REACTIVE => { + // Reactive positioners recompute their position + // when the parent surface's geometry changes. + // Stored for future geometry-aware re-evaluation. + // Cross-referenced with wlroots xdg-positioner.c. + if payload.len() >= 4 { + let reactive = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]) != 0; + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.positioners.entry(object_id).or_default().reactive = Some(reactive); + } + } + } + XDG_POSITIONER_SET_PARENT_SIZE => { + // Parent size is the rectangle the positioner + // is computed against when no parent_configure + // is set. Cross-referenced with wlroots. + if let (Some(w), Some(h)) = (read_payload_i32(payload, 0), read_payload_i32(payload, 1)) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.positioners.entry(object_id).or_default().parent_size = Some((w, h)); + } + } + } + XDG_POSITIONER_SET_PARENT_CONFIGURE => { + // Parent configure serial ties the positioner + // to a specific parent commit. The serial must + // match a previous xdg_surface.configure event. + // Stored for verification when the popup is + // positioned. Cross-referenced with wlroots. + if payload.len() >= 4 { + let serial = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.positioners.entry(object_id).or_default().parent_configure = Some(serial); + } + } + } + _ => { + log::info!( + "redbear-compositor: unhandled xdg_positioner opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_XDG_POPUP => match opcode { + XDG_POPUP_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.object_versions.remove(&object_id); + for surface in client.surfaces.values_mut() { + if let Some(SurfaceRole::Popup(ref ps)) = surface.role { + if ps.object_id == object_id { + surface.role = None; + break; + } + } + } + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + XDG_POPUP_GRAB => { + if payload.len() >= 8 { + let _seat_id = read_payload_u32(payload, 0).unwrap_or(0); + let serial = read_payload_u32(payload, 1).unwrap_or(0); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + for surface in client.surfaces.values_mut() { + if let Some(SurfaceRole::Popup(ref mut ps)) = surface.role { + if ps.object_id == object_id { + ps.grab_serial = Some(serial); + break; + } + } + } + } + } + } + XDG_POPUP_REPOSITION => { + if payload.len() >= 8 { + let positioner_id = read_payload_u32(payload, 0).unwrap_or(0); + let token = read_payload_u32(payload, 1).unwrap_or(0); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + let pos_state = client.positioners.get(&positioner_id).cloned(); + if let Some(ps) = client.surfaces.values_mut() + .filter_map(|s| match &mut s.role { + Some(SurfaceRole::Popup(p)) if p.object_id == object_id => Some(p), + _ => None, + }) + .next() + { + ps.positioner_id = Some(positioner_id); + if let Some(ref pos) = pos_state { + if let Some((w, h)) = pos.size { + self.send_xdg_surface_configure( + client_id, + stream, + object_id, + self.next_serial(), + )?; + } + } + } + } + drop(clients); + self.send_xdg_popup_repositioned(client_id, stream, object_id, token)?; + } + } + _ => { + log::info!( + "redbear-compositor: unhandled xdg_popup opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_POINTER => match opcode { + WL_POINTER_RELEASE => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.object_versions.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + WL_POINTER_SET_CURSOR => { + if let (Some(serial), Some(surface), Some(hotspot_x), Some(hotspot_y)) = ( + read_payload_u32(payload, 0), + read_payload_u32(payload, 1), + read_payload_i32(payload, 2), + read_payload_i32(payload, 3), + ) { + let mut pointer_state = self.pointer_state.lock().expect("pointer_state lock"); + pointer_state.cursor_surface = if surface != 0 { Some(surface) } else { None }; + pointer_state.cursor_serial = Some(serial); + pointer_state.cursor_hotspot = (hotspot_x, hotspot_y); + } + } + _ => { + // Other wl_pointer requests are accepted but currently have no + // additional effect in the software compositing path. + if opcode == WL_POINTER_BUTTON { + if let (Some(serial), Some(button)) = ( + read_payload_u32(payload, 0), + read_payload_u32(payload, 2), + ) { + let mut pointer_state = self.pointer_state.lock().expect("pointer_state lock"); + pointer_state.last_button_serial = Some(serial); + pointer_state.last_button = Some(button); + } + } + } + }, + OBJECT_TYPE_WL_KEYBOARD => match opcode { + WL_KEYBOARD_RELEASE => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + WL_KEYBOARD_KEY | WL_KEYBOARD_ENTER | WL_KEYBOARD_LEAVE | WL_KEYBOARD_MODIFIERS | WL_KEYBOARD_REPEAT_INFO => { + // Keyboard events dispatched to focused client surface when input daemon is wired. + let mut keyboard_state = self.keyboard_state.lock().expect("keyboard_state lock"); + if opcode == WL_KEYBOARD_KEY { + if let (Some(time), Some(keycode), Some(state)) = ( + read_payload_u32(payload, 1), + read_payload_u32(payload, 2), + read_payload_u32(payload, 3), + ) { + keyboard_state.last_time = Some(time); + keyboard_state.last_keycode = Some(keycode); + keyboard_state.last_state = Some(state); + let pressed = state == 1; + keyboard_state.apply_modifier_event(keycode, pressed); + } + } else if opcode == WL_KEYBOARD_MODIFIERS { + if let (Some(depressed), Some(latched), Some(locked), Some(group)) = ( + read_payload_u32(payload, 1), + read_payload_u32(payload, 2), + read_payload_u32(payload, 3), + read_payload_u32(payload, 4), + ) { + keyboard_state.depressed = depressed; + keyboard_state.latched = latched; + keyboard_state.locked = locked; + keyboard_state.group = group; + } + } + if let Some(event) = keyboard_state.pending_events.pop_front() { + let focused_surface = keyboard_state.focused_surface; + drop(keyboard_state); + if let Some(surface_id) = focused_surface { + self.send_keyboard_key_event( + client_id, + stream, + object_id, + surface_id, + event.time, + event.keycode, + event.state, + )?; + } + } + } + _ => { + // No-op for currently unwired keyboard bookkeeping opcodes. + } + }, + OBJECT_TYPE_WL_TOUCH => match opcode { + WL_TOUCH_RELEASE => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + _ => { + log::info!( + "redbear-compositor: unhandled zxdg_decoration_manager_v1 opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_DATA_DEVICE_MANAGER => match opcode { + WL_DATA_DEVICE_MANAGER_CREATE_DATA_SOURCE => { + 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().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, OBJECT_TYPE_WL_DATA_SOURCE); + } + } + } + WL_DATA_DEVICE_MANAGER_GET_DATA_DEVICE => { + 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().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, OBJECT_TYPE_WL_DATA_DEVICE); + } + } + } + _ => { + log::info!( + "redbear-compositor: unhandled data_device_manager opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_ZXDG_DECORATION_MANAGER_V1 => match opcode { + ZXDG_DECORATION_MANAGER_V1_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION => { + if payload.len() >= 8 { + let new_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let toplevel_id = u32::from_le_bytes([ + payload[4], payload[5], payload[6], payload[7], + ]); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, OBJECT_TYPE_ZXDG_TOPLEVEL_DECORATION_V1); + client.decorations.insert(new_id, toplevel_id); + } + drop(clients); + self.send_zxdg_toplevel_decoration_configure( + client_id, + stream, + new_id, + ZXDG_TOPLEVEL_DECORATION_MODE_SERVER_SIDE, + )?; + } + } + _ => { + log::info!( + "redbear-compositor: unhandled zxdg_toplevel_decoration_v1 opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_ZXDG_TOPLEVEL_DECORATION_V1 => match opcode { + ZXDG_TOPLEVEL_DECORATION_V1_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.decorations.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE | ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE => { + self.send_zxdg_toplevel_decoration_configure( + client_id, + stream, + object_id, + ZXDG_TOPLEVEL_DECORATION_MODE_SERVER_SIDE, + )?; + } + _ => { + log::info!( + "redbear-compositor: unhandled wp_viewporter opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WP_VIEWPORTER => match opcode { + WP_VIEWPORTER_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + WP_VIEWPORTER_GET_VIEWPORT => { + if payload.len() >= 8 { + let new_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let surface_id = u32::from_le_bytes([ + payload[4], payload[5], payload[6], payload[7], + ]); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, OBJECT_TYPE_WP_VIEWPORT); + client.viewporters.insert(new_id, ViewportState::default()); + client.xdg_to_surface.insert(new_id, surface_id); + } + } + } + _ => { + log::info!( + "redbear-compositor: unhandled wp_viewport opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WP_VIEWPORT => match opcode { + WP_VIEWPORT_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.viewporters.remove(&object_id); + client.xdg_to_surface.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + WP_VIEWPORT_SET_SOURCE => { + if let (Some(x), Some(y), Some(w), Some(h)) = ( + read_payload_i32(payload, 0), + read_payload_i32(payload, 1), + read_payload_i32(payload, 2), + read_payload_i32(payload, 3), + ) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.viewporters.entry(object_id).or_default().source = Some((x, y, w, h)); + } + } + } + WP_VIEWPORT_SET_DESTINATION => { + if let (Some(w), Some(h)) = (read_payload_i32(payload, 0), read_payload_i32(payload, 1)) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.viewporters.entry(object_id).or_default().destination = Some((w, h)); + } + } + } + _ => { + log::info!( + "redbear-compositor: unhandled wp_viewport opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_ZWP_LINUX_DMABUF_V1 => match opcode { + ZWP_LINUX_DMABUF_V1_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + ZWP_LINUX_DMABUF_V1_CREATE_PARAMS => { + 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().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, OBJECT_TYPE_ZWP_LINUX_BUFFER_PARAMS_V1); + client.dmabuf_params.insert(new_id, DmabufParamsState::default()); + } + drop(clients); + self.send_linux_dmabuf_format( + client_id, + stream, + object_id, + DRM_FORMAT_XRGB8888, + )?; + self.send_linux_dmabuf_format( + client_id, + stream, + object_id, + DRM_FORMAT_ARGB8888, + )?; + } + } + _ => { + log::info!( + "redbear-compositor: unhandled zwp_linux_dmabuf_v1 opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_ZWP_LINUX_BUFFER_PARAMS_V1 => match opcode { + ZWP_LINUX_BUFFER_PARAMS_V1_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.dmabuf_params.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + ZWP_LINUX_BUFFER_PARAMS_V1_ADD => { + if payload.len() >= 24 { + let fd = i32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let plane_idx = u32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]); + let offset = u32::from_le_bytes([payload[8], payload[9], payload[10], payload[11]]); + let stride = u32::from_le_bytes([payload[12], payload[13], payload[14], payload[15]]); + let modifier_hi = u32::from_le_bytes([payload[16], payload[17], payload[18], payload[19]]); + let modifier_lo = u32::from_le_bytes([payload[20], payload[21], payload[22], payload[23]]); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.dmabuf_params.entry(object_id).or_default().planes.push(DmabufPlane { + fd: Some(fd), plane_idx: Some(plane_idx), offset: Some(offset), stride: Some(stride), modifier_hi: Some(modifier_hi), modifier_lo: Some(modifier_lo), + }); + } + } + } + ZWP_LINUX_BUFFER_PARAMS_V1_CREATE | ZWP_LINUX_BUFFER_PARAMS_V1_CREATE_IMMED => { + if let (Some(_width), Some(_height), Some(format), Some(flags)) = ( + read_payload_i32(payload, 0), + read_payload_i32(payload, 1), + read_payload_u32(payload, 2), + read_payload_u32(payload, 3), + ) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + let state = client.dmabuf_params.entry(object_id).or_default(); + state.width = Some(_width); + state.height = Some(_height); + state.format = Some(format); + state.flags = Some(flags); + state.created = true; + } + } + } + _ => { + log::info!( + "redbear-compositor: unhandled zwp_linux_buffer_params_v1 opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WP_PRESENTATION => match opcode { + WP_PRESENTATION_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + WP_PRESENTATION_FEEDBACK => { + if payload.len() >= 8 { + let new_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let surface_id = u32::from_le_bytes([ + payload[4], payload[5], payload[6], payload[7], + ]); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, OBJECT_TYPE_WP_PRESENTATION_FEEDBACK); + client.presentation_feedback.insert( + new_id, + PresentationFeedbackState { + surface_id: if surface_id != 0 { Some(surface_id) } else { None }, + last_feedback_serial: Some(self.next_serial()), + }, + ); + } + drop(clients); + let queue_time_nsec = clock_monotonic_nsec(); + self.pending_feedbacks.lock().expect("pending_feedbacks lock").push(PendingFeedback { + client_id, + feedback_id: new_id, + surface_id, + queue_time_nsec, + }); + } + } + _ => { + log::info!( + "redbear-compositor: unhandled wp_presentation opcode {} on object {}", + opcode, object_id + ); + } + }, +OBJECT_TYPE_WP_PRESENTATION_FEEDBACK => match opcode { + WP_PRESENTATION_FEEDBACK_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.presentation_feedback.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + _ => { + log::info!( + "redbear-compositor: unhandled wp_presentation_feedback opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_ZWLR_LAYER_SHELL_V1 => match opcode { + ZWLR_LAYER_SHELL_V1_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + ZWLR_LAYER_SHELL_V1_GET_LAYER_SURFACE => { + if payload.len() >= 24 { + let new_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let _surface_id = u32::from_le_bytes([ + payload[4], payload[5], payload[6], payload[7], + ]); + let _layer = u32::from_le_bytes([ + payload[8], payload[9], payload[10], payload[11], + ]); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, OBJECT_TYPE_ZWLR_LAYER_SURFACE_V1); + } + drop(clients); + self.send_layer_surface_configure(client_id, stream, new_id, 0)?; + } + } + _ => { + log::info!( + "redbear-compositor: unhandled zwlr_layer_shell_v1 opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_ZWLR_LAYER_SURFACE_V1 => match opcode { + ZWLR_LAYER_SURFACE_V1_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + ZWLR_LAYER_SURFACE_V1_ACK_CONFIGURE => { + let _ = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + } + _ => { + log::info!( + "redbear-compositor: unhandled zwlr_layer_surface_v1 opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_ZWLR_OUTPUT_MANAGER_V1 => match opcode { + ZWLR_OUTPUT_MANAGER_V1_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + ZWLR_OUTPUT_MANAGER_V1_CREATE_CONFIGURATION => { + if payload.len() >= 8 { + let new_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let serial = self.next_serial(); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, OBJECT_TYPE_ZWLR_OUTPUT_CONFIG_V1); + } + drop(clients); + self.send_output_config_serial(client_id, stream, new_id, serial)?; + } + } + _ => { + log::info!( + "redbear-compositor: unhandled zwlr_output_manager_v1 opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_ZWLR_OUTPUT_CONFIG_V1 => match opcode { + ZWLR_OUTPUT_CONFIG_V1_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + ZWLR_OUTPUT_CONFIG_V1_APPLY => { + self.send_output_config_succeeded(client_id, stream, object_id)?; + } + ZWLR_OUTPUT_CONFIG_V1_TEST => { + self.send_output_config_succeeded(client_id, stream, object_id)?; + } + _ => { + log::info!( + "redbear-compositor: unhandled zwlr_output_config_v1 opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_DATA_SOURCE => match opcode { + WL_DATA_SOURCE_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.data_sources.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + WL_DATA_SOURCE_OFFER => { + if let Some(mime_type) = read_payload_string(payload) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.data_sources.entry(object_id) + .or_default() + .mime_types + .push(mime_type.to_string()); + } + } + } + WL_DATA_SOURCE_SET_ACTIONS => { + if let Some(actions) = read_payload_u32(payload, 0) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.data_sources.entry(object_id).or_default().actions = Some(actions); + } + } + } + _ => { + log::info!( + "redbear-compositor: unhandled data_source opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_DATA_DEVICE => match opcode { + WL_DATA_DEVICE_RELEASE => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.data_devices.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + WL_DATA_DEVICE_START_DRAG => { + // Accepted — drag-and-drop requires pointer-grab tracking, not yet wired. + } + WL_DATA_DEVICE_SET_SELECTION => { + // Payload: source_id: u32 (optionally 0 to clear) + if let Some(source_id) = read_payload_u32(payload, 0) { + let new_offer_id = self.alloc_id(); + let mime_types: Vec = { + let clients = self.clients.lock().expect("clients lock"); + clients + .get(&client_id) + .and_then(|c| c.data_sources.get(&source_id)) + .map(|s| s.mime_types.clone()) + .unwrap_or_default() + }; + let source_actions = { + let clients = self.clients.lock().expect("clients lock"); + clients + .get(&client_id) + .and_then(|c| c.data_sources.get(&source_id)) + .and_then(|s| s.actions) + }; + let transfer_buffer = { + let clients = self.clients.lock().expect("clients lock"); + clients + .get(&client_id) + .and_then(|c| c.data_sources.get(&source_id)) + .and_then(|s| s.buffer.clone()) + }; + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_offer_id, OBJECT_TYPE_WL_DATA_OFFER); + client.data_offers.insert( + new_offer_id, + DataOfferState { + source_client_id: client_id, + source_id, + mime_types: mime_types.clone(), + accepted_mime: None, + actions: source_actions, + buffer: transfer_buffer, + finished: false, + }, + ); + let device = client.data_devices.entry(object_id).or_default(); + device.selection_source = if source_id != 0 { Some(source_id) } else { None }; + device.selection_offer = if source_id != 0 { Some(new_offer_id) } else { None }; + } + drop(clients); + let mut msg = Vec::with_capacity(8 + mime_types.len() * 16); + push_header( + &mut msg, + new_offer_id, + WL_DATA_OFFER_OFFER, + mime_types.len() * 16, + ); + for mt in &mime_types { + let mut padded = [0u8; 16]; + let bytes = mt.as_bytes(); + let copy_len = bytes.len().min(16); + padded[..copy_len].copy_from_slice(&bytes[..copy_len]); + msg.extend_from_slice(&padded); + } + self.write_event(client_id, stream, &msg, "wl_data_offer.offer")?; + if let Some(actions) = source_actions { + let mut msg = Vec::with_capacity(16); + push_header( + &mut msg, + new_offer_id, + WL_DATA_OFFER_SOURCE_ACTIONS, + 4, + ); + push_u32(&mut msg, actions); + self.write_event( + client_id, + stream, + &msg, + "wl_data_offer.source_actions", + )?; + } + let mut msg = Vec::with_capacity(8); + push_header( + &mut msg, + object_id, + WL_DATA_DEVICE_DATA_OFFER, + 4, + ); + push_u32(&mut msg, new_offer_id); + self.write_event(client_id, stream, &msg, "wl_data_device.data_offer")?; + let mut msg = Vec::with_capacity(8); + push_header(&mut msg, object_id, WL_DATA_DEVICE_SELECTION, 4); + push_u32(&mut msg, new_offer_id); + self.write_event(client_id, stream, &msg, "wl_data_device.selection")?; + } + } + _ => { + log::info!( + "redbear-compositor: unhandled data_device opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_DATA_OFFER => match opcode { + WL_DATA_OFFER_ACCEPT => { + if payload.len() >= 8 { + let serial = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let mime_type = read_payload_string(&payload[4..]); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + if let Some(offer) = client.data_offers.get_mut(&object_id) { + let _ = serial; + offer.accepted_mime = mime_type.map(|s| s.to_string()); + } + } + } + } + WL_DATA_OFFER_RECEIVE => { + if let Some(mime_bytes) = read_payload_string(payload) { + let mime_str = mime_bytes.to_string(); + let payload_bytes = { + let clients = self.clients.lock().expect("clients lock"); + clients + .get(&client_id) + .and_then(|c| c.data_offers.get(&object_id)) + .filter(|o| o.accepted_mime.as_deref() == Some(mime_str.as_str())) + .and_then(|o| o.buffer.clone()) + }; + match payload_bytes { + Some(bytes) => { + let mut fds = VecDeque::new(); + if let Some(fd) = self.open_pipe_for_payload(&bytes) { + fds.push_back(fd); + } + let mut msg = Vec::with_capacity(8 + mime_bytes.len() + 1); + push_header(&mut msg, object_id, WL_DATA_OFFER_RECEIVE, 0); + let mut padded: Vec = + mime_bytes.bytes().chain(std::iter::once(0)).collect(); + msg.extend_from_slice(&padded); + self.write_event_with_fds( + client_id, + stream, + &msg, + &mut fds, + "wl_data_offer.receive", + )?; + if let Some(fd) = fds.pop_front() { + let _ = unsafe { libc::close(fd) }; + } + } + None => { + let mut msg = Vec::with_capacity(8 + mime_bytes.len() + 1); + push_header(&mut msg, object_id, WL_DATA_OFFER_RECEIVE, 0); + let mut padded: Vec = + mime_bytes.bytes().chain(std::iter::once(0)).collect(); + msg.extend_from_slice(&padded); + self.write_event( + client_id, + stream, + &msg, + "wl_data_offer.receive", + )?; + } + } + } + } + WL_DATA_OFFER_FINISH => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + if let Some(offer) = client.data_offers.get_mut(&object_id) { + offer.finished = true; + } + } + } + WL_DATA_OFFER_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.data_offers.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + _ => { + log::info!( + "redbear-compositor: unhandled data_offer opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_SUBCOMPOSITOR => match opcode { + WL_SUBCOMPOSITOR_GET_SUBSURFACE => { + // Payload: [new_id: u32][surface_id: u32][parent_id: u32] + if payload.len() >= 12 { + let new_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let surface_id = u32::from_le_bytes([ + payload[4], payload[5], payload[6], payload[7], + ]); + let parent_id = u32::from_le_bytes([ + payload[8], payload[9], payload[10], payload[11], + ]); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.insert(new_id, OBJECT_TYPE_WL_SUBSURFACE); + client.subsurfaces.insert(new_id, SubsurfaceState { + surface_id, + parent_surface_id: parent_id, + ..Default::default() + }); + } + } + } + WL_SUBCOMPOSITOR_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + _ => { + log::info!( + "redbear-compositor: unhandled subcompositor opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_SUBSURFACE => match opcode { + WL_SUBSURFACE_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.subsurfaces.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + WL_SUBSURFACE_SET_POSITION => { + if let (Some(x), Some(y)) = (read_payload_i32(payload, 0), read_payload_i32(payload, 1)) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + if let Some(ss) = client.subsurfaces.get_mut(&object_id) { + ss.x = x; + ss.y = y; + } + } + } + } + WL_SUBSURFACE_PLACE_ABOVE => { + if let Some(sibling_id) = read_payload_u32(payload, 0) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + let sibling_z = client.subsurfaces.values() + .find(|ss| ss.surface_id == sibling_id) + .map(|ss| ss.z_index) + .unwrap_or(0); + if let Some(ss) = client.subsurfaces.get_mut(&object_id) { + ss.z_index = sibling_z + 1; + } + } + } + } + WL_SUBSURFACE_PLACE_BELOW => { + if let Some(sibling_id) = read_payload_u32(payload, 0) { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + let sibling_z = client.subsurfaces.values() + .find(|ss| ss.surface_id == sibling_id) + .map(|ss| ss.z_index) + .unwrap_or(0); + if let Some(ss) = client.subsurfaces.get_mut(&object_id) { + ss.z_index = sibling_z - 1; + } + } + } + } + WL_SUBSURFACE_SET_SYNC => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + if let Some(ss) = client.subsurfaces.get_mut(&object_id) { + ss.sync = true; + } + } + } + WL_SUBSURFACE_SET_DESYNC => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + if let Some(ss) = client.subsurfaces.get_mut(&object_id) { + ss.sync = false; + } + } + } + _ => { + log::info!( + "redbear-compositor: unhandled wl_subsurface opcode {} on object {}", + opcode, object_id + ); + } + } + OBJECT_TYPE_WL_REGION => match opcode { + WL_REGION_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + client.regions.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + WL_REGION_ADD | WL_REGION_SUBTRACT => { + if payload.len() >= 16 { + let x = i32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let y = i32::from_le_bytes([ + payload[4], payload[5], payload[6], payload[7], + ]); + let w = i32::from_le_bytes([ + payload[8], payload[9], payload[10], payload[11], + ]); + let h = i32::from_le_bytes([ + payload[12], payload[13], payload[14], payload[15], + ]); + let rect = Rect { x, y, w, h }; + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + let region = client + .regions + .entry(object_id) + .or_insert_with(RegionState::default); + match opcode { + WL_REGION_ADD => region.added.push(rect), + WL_REGION_SUBTRACT => region.subtracted.push(rect), + _ => { + return Err(format!( + "redbear-compositor: unexpected wl_region opcode {} on object {}", + opcode, object_id + )) + } + } + } + } + } + _ => { + log::info!( + "redbear-compositor: unhandled wl_region opcode {} on object {}", + opcode, object_id + ); + } + }, + OBJECT_TYPE_WL_FIXES => match opcode { + WL_FIXES_DESTROY => { + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.objects.remove(&object_id); + } + drop(clients); + self.send_delete_id(client_id, stream, object_id)?; + } + WL_FIXES_DESTROY_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().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + if client.objects.get(®istry_id).copied() + == Some(OBJECT_TYPE_WL_REGISTRY) + { + client.objects.remove(®istry_id); + } + } + drop(clients); + self.send_delete_id(client_id, stream, registry_id)?; + } + } + WL_FIXES_ACK_GLOBAL_REMOVE => { + if payload.len() >= 4 { + let name = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let mut clients = self.clients.lock().expect("clients lock"); + if let Some(client) = clients.get_mut(&client_id) { + client.acked_global_removals.insert(name); + } + } + } + _ => { + log::info!( + "redbear-compositor: unhandled wl_fixes opcode {} on object {}", + opcode, object_id + ); + } + }, + _ => { + log::info!( + "redbear-compositor: unhandled object {} opcode {}", + object_id, opcode + ); + } + } + + offset += msg_size; + } + Ok(()) + } + + pub 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 buffer + 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.buffer_ptr(idx) as *mut u8; + } else { + let mut fb = self.fb_data.lock().expect("fb_data lock"); + fb_stride = self.fb_stride as usize; + fb_ptr = fb.as_mut().as_mut_ptr(); + drop(fb); // release lock before unsafe block + } + } else { + let mut fb = self.fb_data.lock().expect("fb_data lock"); + fb_stride = self.fb_stride as usize; + fb_ptr = fb.as_mut().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(drm_guard) = self.drm.lock() { + if let Some(ref drm) = *drm_guard { + drm.flip(); + } + } + self.frame_seq.fetch_add(1, Ordering::Relaxed); + } + + pub fn send_buffer_release( + &self, + client_id: u32, + stream: &mut UnixStream, + buffer_id: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(8); + push_header(&mut msg, buffer_id, WL_BUFFER_RELEASE, 0); + self.write_event(client_id, stream, &msg, "wl_buffer.release") + } + + pub fn send_xdg_surface_configure( + &self, + client_id: u32, + stream: &mut UnixStream, + surface_id: u32, + serial: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(12); + push_header(&mut msg, surface_id, XDG_SURFACE_CONFIGURE, 4); + push_u32(&mut msg, serial); + self.write_event(client_id, stream, &msg, "xdg_surface.configure") + } + + pub fn send_xdg_toplevel_configure( + &self, + client_id: u32, + stream: &mut UnixStream, + toplevel_id: u32, + ) -> Result<(), String> { + 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); + self.write_event(client_id, stream, &msg, "xdg_toplevel.configure") + } + + pub fn send_xdg_popup_configure( + &self, + client_id: u32, + stream: &mut UnixStream, + popup_id: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(24); + push_header(&mut msg, popup_id, XDG_POPUP_CONFIGURE, 16); + push_i32(&mut msg, 0); + push_i32(&mut msg, 0); + push_i32(&mut msg, self.fb_width as i32); + push_i32(&mut msg, self.fb_height as i32); + self.write_event(client_id, stream, &msg, "xdg_popup.configure") + } + + pub fn send_xdg_popup_repositioned( + &self, + client_id: u32, + stream: &mut UnixStream, + popup_id: u32, + token: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(12); + push_header(&mut msg, popup_id, XDG_POPUP_REPOSITIONED, 4); + push_u32(&mut msg, token); + self.write_event(client_id, stream, &msg, "xdg_popup.repositioned") + } + + #[allow(dead_code)] + pub fn send_xdg_wm_base_ping( + &self, + client_id: u32, + stream: &mut UnixStream, + xdg_wm_base_id: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(12); + push_header(&mut msg, xdg_wm_base_id, XDG_WM_BASE_PING, 4); + push_u32(&mut msg, self.next_serial()); + self.write_event(client_id, stream, &msg, "xdg_wm_base.ping") + } + + pub fn send_shm_format( + &self, + client_id: u32, + stream: &mut UnixStream, + shm_id: u32, + format: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(12); + push_header(&mut msg, shm_id, WL_SHM_FORMAT, 4); + push_u32(&mut msg, format); + self.write_event(client_id, stream, &msg, "wl_shm.format") + } + + pub fn send_output_info( + &self, + client_id: u32, + stream: &mut UnixStream, + output_id: u32, + version: u32, + ) -> Result<(), String> { + { + 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); + self.write_event(client_id, stream, &msg, "wl_output.geometry")?; + } + { + let mut msg = Vec::with_capacity(24); + push_header(&mut msg, output_id, WL_OUTPUT_MODE, 16); + push_u32(&mut msg, 0x3); + push_i32(&mut msg, self.fb_width as i32); + push_i32(&mut msg, self.fb_height as i32); + push_i32(&mut msg, 60_000); + self.write_event(client_id, stream, &msg, "wl_output.mode")?; + } + if version >= 2 { + let mut msg = Vec::with_capacity(12); + push_header(&mut msg, output_id, WL_OUTPUT_SCALE, 4); + push_i32(&mut msg, 1); + self.write_event(client_id, stream, &msg, "wl_output.scale")?; + } + if version >= 4 { + let mut payload = Vec::new(); + push_wayland_string(&mut payload, "RedBear-0"); + let mut msg = Vec::with_capacity(8 + payload.len()); + push_header(&mut msg, output_id, WL_OUTPUT_NAME, payload.len()); + msg.extend_from_slice(&payload); + self.write_event(client_id, stream, &msg, "wl_output.name")?; + } + if version >= 4 { + let mut payload = Vec::new(); + push_wayland_string(&mut payload, "Red Bear OS framebuffer output"); + let mut msg = Vec::with_capacity(8 + payload.len()); + push_header(&mut msg, output_id, WL_OUTPUT_DESCRIPTION, payload.len()); + msg.extend_from_slice(&payload); + self.write_event(client_id, stream, &msg, "wl_output.description")?; + } + if version >= 2 { + let mut msg = Vec::with_capacity(8); + push_header(&mut msg, output_id, WL_OUTPUT_DONE, 0); + self.write_event(client_id, stream, &msg, "wl_output.done")?; + } + Ok(()) + } + + pub fn send_seat_capabilities( + &self, + client_id: u32, + stream: &mut UnixStream, + seat_id: u32, + version: u32, + ) -> Result<(), String> { + { + let mut msg = Vec::with_capacity(12); + push_header(&mut msg, seat_id, WL_SEAT_CAPABILITIES, 4); + push_u32(&mut msg, 0x3); + self.write_event(client_id, stream, &msg, "wl_seat.capabilities")?; + } + if version >= 2 { + 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, WL_SEAT_NAME, payload.len()); + msg.extend_from_slice(&payload); + self.write_event(client_id, stream, &msg, "wl_seat.name")?; + } + Ok(()) + } + + pub fn send_keyboard_setup( + &self, + client_id: u32, + stream: &mut UnixStream, + keyboard_id: u32, + ) -> Result<(), String> { + // Wayland spec: NO_KEYMAP requires size=0, fd=-1. send_with_rights + // cannot transmit a -1 fd (kernel rejects it; empty fds yields 0). + // Redox has no compositor-side XKB v1 keymap generator (see + // 3D-DESKTOP-COMPREHENSIVE-PLAN.md §5.5). + let mut msg = Vec::with_capacity(12); + push_u32(&mut msg, keyboard_id); + push_u32(&mut msg, (12u32 << 16) | u32::from(WL_KEYBOARD_KEYMAP)); + push_u32(&mut msg, WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP); + push_u32(&mut msg, 0); + push_u32(&mut msg, 0xFFFF_FFFF_u32); + if let Err(err) = stream.write_all(&msg) { + self.disconnect_client( + client_id, + stream, + &format!("wl_keyboard.keymap send failed: {err}"), + ); + return Err(format!("wl_keyboard.keymap send failed: {err}")); + } + + { + let mut msg = Vec::with_capacity(16); + push_header(&mut msg, keyboard_id, WL_KEYBOARD_REPEAT_INFO, 8); + push_i32(&mut msg, 0); + push_i32(&mut msg, 0); + self.write_event(client_id, stream, &msg, "wl_keyboard.repeat_info")?; + } + + { + let mut msg = Vec::with_capacity(28); + push_header(&mut msg, keyboard_id, WL_KEYBOARD_MODIFIERS, 20); + let (depressed, latched, locked, group) = { + let kb = self.keyboard_state.lock().expect("keyboard_state lock"); + (kb.depressed, kb.latched, kb.locked, kb.group) + }; + push_u32(&mut msg, self.next_serial()); + push_u32(&mut msg, depressed); + push_u32(&mut msg, latched); + push_u32(&mut msg, locked); + push_u32(&mut msg, group); + self.write_event(client_id, stream, &msg, "wl_keyboard.modifiers")?; + } + Ok(()) + } + + pub fn send_keyboard_key_event( + &self, + client_id: u32, + stream: &mut UnixStream, + keyboard_id: u32, + _surface_id: u32, + time: u32, + keycode: u32, + state: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(24); + push_header(&mut msg, keyboard_id, WL_KEYBOARD_KEY, 16); + push_u32(&mut msg, self.next_serial()); + push_u32(&mut msg, time); + push_u32(&mut msg, keycode); + push_u32(&mut msg, state); + self.write_event(client_id, stream, &msg, "wl_keyboard.key") + } + + pub fn send_keyboard_modifiers( + &self, + client_id: u32, + stream: &mut UnixStream, + keyboard_id: u32, + ) -> Result<(), String> { + let (depressed, latched, locked, group) = { + let kb = self.keyboard_state.lock().expect("keyboard_state lock"); + (kb.depressed, kb.latched, kb.locked, kb.group) + }; + let mut msg = Vec::with_capacity(28); + push_header(&mut msg, keyboard_id, WL_KEYBOARD_MODIFIERS, 20); + push_u32(&mut msg, self.next_serial()); + push_u32(&mut msg, depressed); + push_u32(&mut msg, latched); + push_u32(&mut msg, locked); + push_u32(&mut msg, group); + self.write_event(client_id, stream, &msg, "wl_keyboard.modifiers") + } + + pub fn set_modifier_state( + &self, + depressed: u32, + latched: u32, + locked: u32, + group: u32, + ) { + let mut kb = self.keyboard_state.lock().expect("keyboard_state lock"); + kb.depressed = depressed; + kb.latched = latched; + kb.locked = locked; + kb.group = group; + } + + pub fn send_zxdg_toplevel_decoration_configure( + &self, + client_id: u32, + stream: &mut UnixStream, + decoration_id: u32, + mode: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(12); + push_header(&mut msg, decoration_id, ZXDG_TOPLEVEL_DECORATION_V1_CONFIGURE, 4); + push_u32(&mut msg, mode); + self.write_event(client_id, stream, &msg, "zxdg_toplevel_decoration_v1.configure") + } + + pub fn send_linux_dmabuf_format( + &self, + client_id: u32, + stream: &mut UnixStream, + dmabuf_id: u32, + format: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(12); + push_header(&mut msg, dmabuf_id, ZWP_LINUX_DMABUF_V1_FORMAT, 4); + push_u32(&mut msg, format); + self.write_event(client_id, stream, &msg, "zwp_linux_dmabuf_v1.format") + } + + pub fn send_presentation_feedback_discarded( + &self, + client_id: u32, + stream: &mut UnixStream, + feedback_id: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(8); + push_header(&mut msg, feedback_id, WP_PRESENTATION_FEEDBACK_DISCARDED, 0); + self.write_event(client_id, stream, &msg, "wp_presentation_feedback.discarded") + } + + pub fn send_layer_surface_configure( + &self, + client_id: u32, + stream: &mut UnixStream, + surface_id: u32, + _layer: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(16); + push_header(&mut msg, surface_id, ZWLR_LAYER_SURFACE_V1_CONFIGURE_EVENT, 8); + push_u32(&mut msg, self.next_serial()); + push_u32(&mut msg, 0); + self.write_event(client_id, stream, &msg, "zwlr_layer_surface_v1.configure") + } + + pub fn send_output_config_serial( + &self, + client_id: u32, + stream: &mut UnixStream, + config_id: u32, + serial: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(8); + push_header(&mut msg, config_id, ZWLR_OUTPUT_CONFIG_HEAD_V1_EVENT, 0); + let _ = serial; + self.write_event(client_id, stream, &msg, "zwlr_output_config_head_v1.serial") + } + + pub fn send_output_config_succeeded( + &self, + client_id: u32, + stream: &mut UnixStream, + config_id: u32, + ) -> Result<(), String> { + let mut msg = Vec::with_capacity(8); + push_header(&mut msg, config_id, ZWLR_OUTPUT_CONFIG_V1_SUCCEEDED_EVENT, 0); + self.write_event(client_id, stream, &msg, "zwlr_output_config_v1.succeeded") + } + + pub fn send_presentation_feedback_presented( + &self, + client_id: u32, + stream: &mut UnixStream, + feedback_id: u32, + presented_nsec: u64, + refresh_nsec: u64, + high_crtc: u32, + low_crtc: u32, + ) -> Result<(), String> { + let (sec, nsec) = nsec_to_clock_pair(presented_nsec); + let mut msg = Vec::with_capacity(40); + push_header(&mut msg, feedback_id, WP_PRESENTATION_FEEDBACK_PRESENTED, 32); + push_u32(&mut msg, sec); + push_u32(&mut msg, nsec); + push_u32(&mut msg, (refresh_nsec >> 16) as u32); + push_u32(&mut msg, (refresh_nsec & 0xFFFF) as u32); + push_u32(&mut msg, high_crtc); + push_u32(&mut msg, low_crtc); + push_u32(&mut msg, 1); + push_u32(&mut msg, 0); + self.write_event(client_id, stream, &msg, "wp_presentation_feedback.presented") + } + + pub fn drain_pending_feedbacks( + &self, + client_id: u32, + stream: &mut UnixStream, + ) -> Result<(), String> { + let drained: Vec = { + let mut queue = self.pending_feedbacks.lock().expect("pending_feedbacks lock"); + let mut matching = Vec::new(); + for fb in queue.iter() { + if fb.client_id == client_id { + matching.push(PendingFeedback { + client_id: fb.client_id, + feedback_id: fb.feedback_id, + surface_id: fb.surface_id, + queue_time_nsec: fb.queue_time_nsec, + }); + } + } + queue.retain(|fb| fb.client_id != client_id); + matching + }; + let now = clock_monotonic_nsec(); + let frame_seq = self.frame_seq.load(Ordering::Relaxed); + let frame_hi = (frame_seq >> 32) as u32; + let frame_lo = frame_seq as u32; + for fb in &drained { + if now >= fb.queue_time_nsec { + self.send_presentation_feedback_presented( + client_id, + stream, + fb.feedback_id, + now, + self.refresh_nsec, + frame_hi, + frame_lo, + )?; + } else { + self.pending_feedbacks.lock().expect("pending_feedbacks lock").push(PendingFeedback { + client_id: fb.client_id, + feedback_id: fb.feedback_id, + surface_id: fb.surface_id, + queue_time_nsec: fb.queue_time_nsec, + }); + } + } + Ok(()) + } +} + +fn clock_monotonic_nsec() -> u64 { + // Use the libc crate's binding (already a dependency, used throughout this + // file). A hand-rolled `extern "C" fn libc_clock_gettime` was undefined at + // link time: relibc exports `clock_gettime`, not `libc_clock_gettime`. + let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 }; + unsafe { + libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts); + } + ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64 +} + +fn nsec_to_clock_pair(nsec: u64) -> (u32, u32) { + let sec = (nsec / 1_000_000_000) as u64; + let nsec_remainder = (nsec % 1_000_000_000) as u32; + let hi = (sec >> 32) as u32; + let lo = (sec & 0xFFFFFFFF) as u32; + (hi, lo) +} + + +