From 68e4531a452a949e66e41a3c3e4a21f1a7e1f23e Mon Sep 17 00:00:00 2001 From: Vasilito Date: Wed, 29 Apr 2026 14:53:09 +0100 Subject: [PATCH] feat: compositor enhancements + kirigami cmake + knewstuff fixes Wave A/B background task output: - redbear-compositor: enhanced protocol handling, integration tests - kirigami: updated CMakeLists - knewstuff: recipe refinements --- local/recipes/kde/kf6-knewstuff/recipe.toml | 97 ++-- .../kde/kirigami/source/src/CMakeLists.txt | 4 +- .../src/bin/redbear-compositor-check.rs | 454 ++++++++++++++++-- .../redbear-compositor/source/src/main.rs | 424 +++++++++++----- .../source/tests/integration_test.rs | 183 ++++--- 5 files changed, 876 insertions(+), 286 deletions(-) diff --git a/local/recipes/kde/kf6-knewstuff/recipe.toml b/local/recipes/kde/kf6-knewstuff/recipe.toml index e427367a..265e2023 100644 --- a/local/recipes/kde/kf6-knewstuff/recipe.toml +++ b/local/recipes/kde/kf6-knewstuff/recipe.toml @@ -1,5 +1,4 @@ -# KNewStuff — framework for downloading and sharing data. Real reduced build (NewStuffCore only, QML disabled). -# Full NewStuffQuick/NewStuffWidgets require Qt6Quick which is not yet QML-proven. +#TODO: KNewStuff — attempt an honest core-only build on Redox. Qt Quick, widgets, tools, and Kirigami-facing surfaces stay disabled; the remaining hard blockers are the missing KF6Attica package in-tree and the still-disabled QtNetwork surface in qtbase. [source] tar = "https://invent.kde.org/frameworks/knewstuff/-/archive/v6.10.0/knewstuff-v6.10.0.tar.gz" @@ -7,11 +6,12 @@ tar = "https://invent.kde.org/frameworks/knewstuff/-/archive/v6.10.0/knewstuff-v template = "custom" dependencies = [ "qtbase", - "qtdeclarative", "kf6-extra-cmake-modules", "kf6-kcoreaddons", "kf6-ki18n", "kf6-kconfig", + "kf6-karchive", + "kf6-kpackage", ] script = """ DYNAMIC_INIT @@ -24,67 +24,40 @@ for qtdir in plugins mkspecs metatypes modules; do fi done -BUILD_DIR="${COOKBOOK_SOURCE}/redox_build" -mkdir -p "${BUILD_DIR}" +sed -i 's/^include(ECMQmlModule)/# include(ECMQmlModule) # disabled for Redox core-only build/' \ + "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true +sed -i 's/COMPONENTS Gui Widgets Xml Qml Quick QuickWidgets/COMPONENTS Gui Xml/' \ + "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true +sed -i 's/^find_package(KF6Kirigami2.*/# find_package(KF6Kirigami2 disabled for Redox core-only build)/' \ + "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true +sed -i 's/^ki18n_install(po)/#ki18n_install(po)/' \ + "${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true +sed -i 's/^add_subdirectory(qtquick)/# add_subdirectory(qtquick) # disabled for Redox core-only build/' \ + "${COOKBOOK_SOURCE}/src/CMakeLists.txt" 2>/dev/null || true +sed -i 's/^add_subdirectory(tools)/# add_subdirectory(tools) # disabled for Redox core-only build/' \ + "${COOKBOOK_SOURCE}/src/CMakeLists.txt" 2>/dev/null || true +sed -i 's/^add_subdirectory(widgets)/# add_subdirectory(widgets) # disabled for Redox core-only build/' \ + "${COOKBOOK_SOURCE}/src/CMakeLists.txt" 2>/dev/null || true -cmake -B "${BUILD_DIR}" -S "${COOKBOOK_SOURCE}" \ - -DCMAKE_TOOLCHAIN_FILE="${COOKBOOK_SYSROOT}/usr/share/cmake/redox.cmake" \ - -DCMAKE_INSTALL_PREFIX="${COOKBOOK_STAGE}/usr" \ - -DCMAKE_PREFIX_PATH="${COOKBOOK_STAGE}/usr;${COOKBOOK_SYSROOT}/usr;${HOST_BUILD}" \ - -DBUILD_SHARED_LIBS=OFF \ - -DBUILD_TESTING=OFF \ - -DKF6_HOST_TOOLING="${HOST_BUILD}/lib/cmake" \ - -DBUILD_WITH_QML=OFF \ - -DBUILD_DESKTOPTOJSON=OFF \ - -DKF_NEWSTUFF_BUILD_CORE=ON \ - -DKF_NEWSTUFF_BUILD_QUICK=OFF \ - -DKF_NEWSTUFF_BUILD_WIDGETS=OFF \ - -DQT_MAJOR_VERSION=6 \ +rm -f CMakeCache.txt +rm -rf CMakeFiles + +cmake "${COOKBOOK_SOURCE}" \ + -DCMAKE_TOOLCHAIN_FILE="${COOKBOOK_ROOT}/local/recipes/qt/redox-toolchain.cmake" \ + -DQT_HOST_PATH="${HOST_BUILD}" \ + -DCMAKE_INSTALL_PREFIX=/usr \ -DCMAKE_BUILD_TYPE=Release \ - || { - echo "=== KNewStuff cmake configure failed — falling back to stub configs ===" - STAGE="${COOKBOOK_STAGE}/usr" - mkdir -p "${STAGE}/lib/cmake/KF6NewStuff" - cat > "${STAGE}/lib/cmake/KF6NewStuff/KF6NewStuffConfig.cmake" << 'EOFCFG' -add_library(KF6::NewStuff INTERFACE IMPORTED) -add_library(KF6::NewStuffCore INTERFACE IMPORTED) -add_library(KF6::NewStuffQuick INTERFACE IMPORTED) -add_library(KF6::NewStuffWidgets INTERFACE IMPORTED) -EOFCFG - cat > "${STAGE}/lib/cmake/KF6NewStuff/KF6NewStuffConfigVersion.cmake" << 'EOFVER' -set(PACKAGE_VERSION "6.10.0") -set(PACKAGE_VERSION_COMPATIBLE TRUE) -EOFVER - cat > "${STAGE}/lib/cmake/KF6NewStuff/KF6NewStuffTargets.cmake" << 'EOFTGT' -add_library(KF6::NewStuff INTERFACE IMPORTED) -add_library(KF6::NewStuffCore INTERFACE IMPORTED) -add_library(KF6::NewStuffQuick INTERFACE IMPORTED) -add_library(KF6::NewStuffWidgets INTERFACE IMPORTED) -EOFTGT - mkdir -p "${STAGE}/lib" - echo "/* dummy */" > "${STAGE}/lib/libKF6NewStuff.a" - echo "=== KNewStuff stub installed (cmake configure failed) ===" - exit 0 - } + -DCMAKE_PREFIX_PATH="${COOKBOOK_SYSROOT}:${COOKBOOK_STAGE}/usr/lib/cmake" \ + -DBUILD_TESTING=OFF \ + -DBUILD_QCH=OFF \ + -DBUILD_DESIGNERPLUGIN=OFF \ + -Wno-dev -cmake --build "${BUILD_DIR}" -j "${COOKBOOK_MAKE_JOBS}" || { - echo "=== KNewStuff build failed — falling back to stub ===" - STAGE="${COOKBOOK_STAGE}/usr" - mkdir -p "${STAGE}/lib/cmake/KF6NewStuff" - cat > "${STAGE}/lib/cmake/KF6NewStuff/KF6NewStuffConfig.cmake" << 'EOFCFG' -add_library(KF6::NewStuff INTERFACE IMPORTED) -add_library(KF6::NewStuffCore INTERFACE IMPORTED) -EOFCFG - cat > "${STAGE}/lib/cmake/KF6NewStuff/KF6NewStuffConfigVersion.cmake" << 'EOFVER' -set(PACKAGE_VERSION "6.10.0") -set(PACKAGE_VERSION_COMPATIBLE TRUE) -EOFVER - mkdir -p "${STAGE}/lib" - echo "/* dummy */" > "${STAGE}/lib/libKF6NewStuff.a" - echo "=== KNewStuff stub installed (build failed) ===" - exit 0 -} +cmake --build . -j${COOKBOOK_MAKE_JOBS} +cmake --install . --prefix "${COOKBOOK_STAGE}/usr" -cmake --install "${BUILD_DIR}" -echo "=== KNewStuff real build (Core only, QML disabled) ===" +for lib in "${COOKBOOK_STAGE}/usr/lib/"libKF6*.so.*; do + [ -f "${lib}" ] || continue + patchelf --remove-rpath "${lib}" 2>/dev/null || true +done """ diff --git a/local/recipes/kde/kirigami/source/src/CMakeLists.txt b/local/recipes/kde/kirigami/source/src/CMakeLists.txt index fe795e8a..0511d737 100644 --- a/local/recipes/kde/kirigami/source/src/CMakeLists.txt +++ b/local/recipes/kde/kirigami/source/src/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.16) -# Build only C++ core, no QML modules -add_library(Kirigami STATIC) +# Build only the non-QML C++ core for now. +add_library(Kirigami) add_library(KF6::Kirigami ALIAS Kirigami) # Core C++ sources that don't require QML/QtQuick diff --git a/local/recipes/wayland/redbear-compositor/source/src/bin/redbear-compositor-check.rs b/local/recipes/wayland/redbear-compositor/source/src/bin/redbear-compositor-check.rs index 6cbd5ae0..7f92dd3d 100644 --- a/local/recipes/wayland/redbear-compositor/source/src/bin/redbear-compositor-check.rs +++ b/local/recipes/wayland/redbear-compositor/source/src/bin/redbear-compositor-check.rs @@ -1,60 +1,427 @@ // Red Bear Compositor Runtime Check — verifies the compositor and greeter surface are healthy. // Usage: redbear-compositor-check [--verbose] -use std::os::unix::net::UnixStream; +use std::collections::HashMap; +use std::fs::OpenOptions; use std::io::{Read, Write}; +use std::mem; +use std::os::fd::AsRawFd; +use std::os::unix::net::UnixStream; use std::time::Duration; -fn check_wayland_socket() -> Result<(), String> { - let runtime_dir = std::env::var("XDG_RUNTIME_DIR") - .unwrap_or_else(|_| "/tmp/run/redbear-greeter".into()); - let display = std::env::var("WAYLAND_DISPLAY") - .unwrap_or_else(|_| "wayland-0".into()); - let socket_path = format!("{}/{}", runtime_dir, display); +const WL_DISPLAY_SYNC: u16 = 0; +const WL_DISPLAY_GET_REGISTRY: u16 = 1; +const WL_REGISTRY_BIND: u16 = 0; +const WL_REGISTRY_GLOBAL: u16 = 0; +const WL_COMPOSITOR_CREATE_SURFACE: u16 = 0; +const WL_SHM_CREATE_POOL: u16 = 0; +const WL_SHM_FORMAT: u16 = 0; +const WL_SHM_POOL_CREATE_BUFFER: u16 = 0; +const WL_SURFACE_ATTACH: u16 = 0; +const WL_SURFACE_COMMIT: u16 = 5; +const WL_CALLBACK_DONE: u16 = 0; +const XDG_WM_BASE_GET_XDG_SURFACE: u16 = 2; +const XDG_SURFACE_GET_TOPLEVEL: u16 = 1; +const XDG_SURFACE_ACK_CONFIGURE: u16 = 4; +const XDG_SURFACE_CONFIGURE: u16 = 0; +const XDG_TOPLEVEL_CONFIGURE: u16 = 0; +const WL_SHM_FORMAT_XRGB8888: u32 = 1; - if !std::path::Path::new(&socket_path).exists() { - return Err(format!("Wayland socket {} does not exist", socket_path)); +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_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); + while buf.len() % 4 != 0 { + buf.push(0); + } +} + +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 mut stream = UnixStream::connect(&socket_path) - .map_err(|e| format!("failed to connect to {}: {}", socket_path, e))?; - stream.set_read_timeout(Some(Duration::from_secs(2))) - .map_err(|e| format!("failed to set timeout: {}", e))?; + let value = u32::from_le_bytes([ + data[*cursor], + data[*cursor + 1], + data[*cursor + 2], + data[*cursor + 3], + ]); + *cursor += 4; + Ok(value) +} - // Send wl_display.sync request to verify protocol - let display_id = 1u32; - let callback_id = 2u32; - let mut msg = Vec::new(); - msg.extend_from_slice(&display_id.to_ne_bytes()); - let size = 12u32; - let opcode = 0u16; // wl_display.sync - msg.extend_from_slice(&((size << 16) | opcode as u32).to_ne_bytes()); - msg.extend_from_slice(&callback_id.to_ne_bytes()); - stream.write_all(&msg) - .map_err(|e| format!("wl_display.sync failed: {}", e))?; +fn read_wayland_string(data: &[u8], cursor: &mut usize) -> Result { + let length = read_u32(data, cursor)? as usize; + if length == 0 { + return Ok(String::new()); + } + if *cursor + length > data.len() { + return Err(String::from("unexpected end of message while reading string")); + } - // Read response - let mut buf = [0u8; 256]; - let n = stream.read(&mut buf) - .map_err(|e| format!("read failed: {}", e))?; + 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; + } - if n < 8 { - return Err(format!("short response: {} bytes", n)); + std::str::from_utf8(&bytes[..string_len]) + .map(str::to_owned) + .map_err(|err| format!("invalid UTF-8 in Wayland string: {err}")) +} + +struct WaylandProbe { + stream: UnixStream, + next_id: u32, +} + +impl WaylandProbe { + fn connect(socket_path: &str) -> Result { + if !std::path::Path::new(socket_path).exists() { + return Err(format!("Wayland socket {} does not exist", socket_path)); + } + + let stream = UnixStream::connect(socket_path) + .map_err(|e| format!("failed to connect to {}: {}", socket_path, e))?; + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .map_err(|e| format!("failed to set timeout: {}", e))?; + + Ok(Self { stream, next_id: 2 }) + } + + fn alloc_id(&mut self) -> u32 { + let id = self.next_id; + self.next_id += 1; + id + } + + fn send_message(&mut self, object_id: u32, opcode: u16, payload: &[u8]) -> Result<(), String> { + 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); + self.stream + .write_all(&msg) + .map_err(|e| format!("write failed: {}", e)) + } + + fn send_message_with_fds( + &mut self, + object_id: u32, + opcode: u16, + payload: &[u8], + fds: &[i32], + ) -> Result<(), String> { + if fds.is_empty() { + return self.send_message(object_id, opcode, payload); + } + + 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); + + 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 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, + }; + + unsafe { + let cmsg = libc::CMSG_FIRSTHDR(&header); + if cmsg.is_null() { + return Err(String::from("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(self.stream.as_raw_fd(), &header, 0) }; + if written < 0 { + return Err(format!("sendmsg failed: {}", std::io::Error::last_os_error())); + } + if written as usize != msg.len() { + return Err(format!("short sendmsg write: expected {}, got {}", msg.len(), written)); + } + + Ok(()) + } + + fn read_message(&mut self) -> Result<(u32, u16, Vec), String> { + let mut header = [0u8; 8]; + self.stream + .read_exact(&mut header) + .map_err(|e| format!("read failed: {}", e))?; + let object_id = u32::from_le_bytes([header[0], header[1], header[2], header[3]]); + let size_opcode = u32::from_le_bytes([header[4], header[5], header[6], header[7]]); + let size = ((size_opcode >> 16) & 0xFFFF) as usize; + let opcode = (size_opcode & 0xFFFF) as u16; + let mut payload = vec![0u8; size.saturating_sub(8)]; + if size > 8 { + self.stream + .read_exact(&mut payload) + .map_err(|e| format!("read payload failed: {}", e))?; + } + Ok((object_id, opcode, payload)) + } + + fn sync(&mut self) -> Result { + let callback_id = self.alloc_id(); + self.send_message(1, WL_DISPLAY_SYNC, &callback_id.to_le_bytes())?; + Ok(callback_id) + } + + fn get_registry(&mut self) -> Result { + let registry_id = self.alloc_id(); + self.send_message(1, WL_DISPLAY_GET_REGISTRY, ®istry_id.to_le_bytes())?; + Ok(registry_id) + } + + fn bind(&mut self, registry_id: u32, name: u32, interface: &str, version: u32) -> Result { + let new_id = self.alloc_id(); + let mut payload = Vec::new(); + push_u32(&mut payload, name); + push_wayland_string(&mut payload, interface); + push_u32(&mut payload, version); + push_u32(&mut payload, new_id); + self.send_message(registry_id, WL_REGISTRY_BIND, &payload)?; + Ok(new_id) + } +} + +fn collect_globals(probe: &mut WaylandProbe) -> Result, String> { + let registry_id = probe.get_registry()?; + let mut globals = HashMap::new(); + + for _ in 0..6 { + let (object_id, opcode, payload) = probe.read_message()?; + if object_id != registry_id || opcode != WL_REGISTRY_GLOBAL { + return Err(format!( + "unexpected registry event: object={} opcode={}", + object_id, opcode + )); + } + + let mut cursor = 0; + let name = read_u32(&payload, &mut cursor)?; + let interface = read_wayland_string(&payload, &mut cursor)?; + let _version = read_u32(&payload, &mut cursor)?; + globals.insert(interface, name); + } + + Ok(globals) +} + +fn expect_shm_formats(probe: &mut WaylandProbe, shm_id: u32) -> Result<(), String> { + let mut formats = Vec::new(); + + for _ in 0..2 { + let (object_id, opcode, payload) = probe.read_message()?; + if object_id != shm_id || opcode != WL_SHM_FORMAT || payload.len() != 4 { + return Err(format!( + "unexpected wl_shm event: object={} opcode={} payload_len={}", + object_id, + opcode, + payload.len() + )); + } + formats.push(u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]])); + } + + if !formats.contains(&0) || !formats.contains(&1) { + return Err(format!("wl_shm.format list incomplete: {:?}", formats)); } Ok(()) } +fn expect_xdg_configure(probe: &mut WaylandProbe, toplevel_id: u32, xdg_surface_id: u32) -> Result { + let (object_id, opcode, payload) = probe.read_message()?; + if object_id != toplevel_id || opcode != XDG_TOPLEVEL_CONFIGURE { + return Err(format!( + "unexpected xdg_toplevel event: object={} opcode={}", + object_id, opcode + )); + } + if payload.len() < 12 { + return Err(format!("short xdg_toplevel.configure payload: {} bytes", payload.len())); + } + + let states_len = u32::from_le_bytes([payload[8], payload[9], payload[10], payload[11]]) as usize; + if payload.len() != 12 + states_len { + return Err(format!( + "invalid xdg_toplevel.configure payload length: {} (states_len={})", + payload.len(), states_len + )); + } + if states_len % 4 != 0 { + return Err(format!("invalid xdg_toplevel.configure states array length: {}", states_len)); + } + + let (object_id, opcode, payload) = probe.read_message()?; + if object_id != xdg_surface_id || opcode != XDG_SURFACE_CONFIGURE || payload.len() != 4 { + return Err(format!( + "unexpected xdg_surface event: object={} opcode={} payload_len={}", + object_id, + opcode, + payload.len() + )); + } + + Ok(u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]])) +} + +fn exercise_shm_pool(probe: &mut WaylandProbe, shm_id: u32, surface_id: u32) -> Result<(), String> { + let temp_path = std::env::temp_dir().join(format!( + "redbear-compositor-check-{}-{}.shm", + std::process::id(), + surface_id + )); + let file = OpenOptions::new() + .create(true) + .read(true) + .write(true) + .truncate(true) + .open(&temp_path) + .map_err(|err| format!("failed to create temp SHM file: {err}"))?; + file.set_len(16) + .map_err(|err| format!("failed to size temp SHM file: {err}"))?; + + let mut file = file; + file.write_all(&[0x40, 0x40, 0xFF, 0xFF, 0x40, 0x40, 0xFF, 0xFF, 0x40, 0x40, 0xFF, 0xFF, 0x40, 0x40, 0xFF, 0xFF]) + .map_err(|err| format!("failed to seed temp SHM file: {err}"))?; + + let pool_id = probe.alloc_id(); + let mut payload = Vec::new(); + push_u32(&mut payload, pool_id); + push_i32(&mut payload, 16); + probe.send_message_with_fds(shm_id, WL_SHM_CREATE_POOL, &payload, &[file.as_raw_fd()])?; + + let buffer_id = probe.alloc_id(); + let mut payload = Vec::new(); + push_u32(&mut payload, buffer_id); + push_u32(&mut payload, 0); + push_i32(&mut payload, 2); + push_i32(&mut payload, 2); + push_i32(&mut payload, 8); + push_u32(&mut payload, WL_SHM_FORMAT_XRGB8888); + probe.send_message(pool_id, WL_SHM_POOL_CREATE_BUFFER, &payload)?; + + let mut payload = Vec::new(); + push_u32(&mut payload, buffer_id); + push_i32(&mut payload, 0); + push_i32(&mut payload, 0); + probe.send_message(surface_id, WL_SURFACE_ATTACH, &payload)?; + probe.send_message(surface_id, WL_SURFACE_COMMIT, &[])?; + + let callback_id = probe.sync()?; + let (object_id, opcode, payload) = probe.read_message()?; + let _ = std::fs::remove_file(&temp_path); + + if object_id != callback_id || opcode != WL_CALLBACK_DONE || payload.len() != 4 { + return Err(format!( + "unexpected callback response after SHM commit: object={} opcode={} payload_len={}", + object_id, + opcode, + payload.len() + )); + } + + Ok(()) +} + +fn check_wayland_socket() -> Result<(), String> { + let runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp/run/redbear-greeter".into()); + let display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".into()); + let socket_path = format!("{}/{}", runtime_dir, display); + + let mut probe = WaylandProbe::connect(&socket_path)?; + let globals = collect_globals(&mut probe)?; + + let registry_id = 2; + let compositor_name = *globals + .get("wl_compositor") + .ok_or_else(|| String::from("wl_compositor global missing"))?; + let shm_name = *globals + .get("wl_shm") + .ok_or_else(|| String::from("wl_shm global missing"))?; + let xdg_name = *globals + .get("xdg_wm_base") + .ok_or_else(|| String::from("xdg_wm_base global missing"))?; + + let compositor_id = probe.bind(registry_id, compositor_name, "wl_compositor", 4)?; + let shm_id = probe.bind(registry_id, shm_name, "wl_shm", 1)?; + let xdg_wm_base_id = probe.bind(registry_id, xdg_name, "xdg_wm_base", 1)?; + + expect_shm_formats(&mut probe, shm_id)?; + + let surface_id = probe.alloc_id(); + probe.send_message(compositor_id, WL_COMPOSITOR_CREATE_SURFACE, &surface_id.to_le_bytes())?; + + let xdg_surface_id = probe.alloc_id(); + let mut payload = Vec::new(); + push_u32(&mut payload, xdg_surface_id); + push_u32(&mut payload, surface_id); + probe.send_message(xdg_wm_base_id, XDG_WM_BASE_GET_XDG_SURFACE, &payload)?; + + let toplevel_id = probe.alloc_id(); + probe.send_message(xdg_surface_id, XDG_SURFACE_GET_TOPLEVEL, &toplevel_id.to_le_bytes())?; + let serial = expect_xdg_configure(&mut probe, toplevel_id, xdg_surface_id)?; + probe.send_message(xdg_surface_id, XDG_SURFACE_ACK_CONFIGURE, &serial.to_le_bytes())?; + + exercise_shm_pool(&mut probe, shm_id, surface_id) +} + fn check_binaries() -> Result<(), Vec> { let mut missing = Vec::new(); - for bin in &["/usr/bin/redbear-compositor", "/usr/bin/redbear-greeterd", - "/usr/bin/redbear-greeter-ui", "/usr/bin/redbear-authd", - "/usr/bin/kwin_wayland_wrapper"] { + for bin in &[ + "/usr/bin/redbear-compositor", + "/usr/bin/redbear-greeterd", + "/usr/bin/redbear-greeter-ui", + "/usr/bin/redbear-authd", + "/usr/bin/kwin_wayland_wrapper", + ] { if !std::path::Path::new(bin).exists() { missing.push(bin.to_string()); } } - if missing.is_empty() { Ok(()) } else { Err(missing) } + if missing.is_empty() { + Ok(()) + } else { + Err(missing) + } } fn check_framebuffer() -> Result<(), String> { @@ -89,7 +456,11 @@ fn check_services() -> Result<(), Vec> { issues.push(format!("{} not found at {}", name, path)); } } - if issues.is_empty() { Ok(()) } else { Err(issues) } + if issues.is_empty() { + Ok(()) + } else { + Err(issues) + } } fn main() { @@ -113,7 +484,9 @@ fn main() { ($label:expr, $check:expr, vec) => { match $check { Ok(()) => { - if verbose { println!(" PASS {}", $label); } + if verbose { + println!(" PASS {}", $label); + } } Err(errs) => { for e in errs { @@ -143,14 +516,17 @@ fn main() { check!("runtime services", check_services(), vec); if verbose { - println!(" Checking Wayland socket..."); + println!(" Checking Wayland protocol features..."); } - check!("Wayland compositor socket", check_wayland_socket()); + check!("Wayland protocol surface", check_wayland_socket()); if exit == 0 { println!("redbear-compositor-check: all checks passed"); } else { - eprintln!("redbear-compositor-check: {} check(s) failed", if exit == 1 { "1" } else { "some" }); + eprintln!( + "redbear-compositor-check: {} check(s) failed", + if exit == 1 { "1" } else { "some" } + ); std::process::exit(1); } } diff --git a/local/recipes/wayland/redbear-compositor/source/src/main.rs b/local/recipes/wayland/redbear-compositor/source/src/main.rs index 131defc4..36ecf3d5 100644 --- a/local/recipes/wayland/redbear-compositor/source/src/main.rs +++ b/local/recipes/wayland/redbear-compositor/source/src/main.rs @@ -20,7 +20,10 @@ use std::io::{Read, Seek, SeekFrom, Write}; use std::mem; use std::os::fd::{AsRawFd, FromRawFd, RawFd}; use std::os::unix::net::{UnixListener, UnixStream}; -use std::sync::{atomic::{AtomicU32, Ordering}, Mutex}; +use std::sync::{ + atomic::{AtomicU32, Ordering}, + Mutex, +}; fn map_framebuffer(_phys: usize, size: usize) -> Vec { vec![0u8; size] @@ -75,11 +78,16 @@ fn read_wayland_string(data: &[u8], cursor: &mut usize) -> Result data.len() { - return Err(String::from("unexpected end of message while reading string")); + 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()); + let string_len = bytes + .iter() + .position(|byte| *byte == 0) + .unwrap_or(bytes.len()); *cursor += length; while *cursor % 4 != 0 { *cursor += 1; @@ -90,7 +98,10 @@ fn read_wayland_string(data: &[u8], cursor: &mut usize) -> Result std::io::Result<(usize, VecDeque)> { +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(), @@ -118,7 +129,8 @@ fn recv_with_rights(stream: &mut UnixStream, data: &mut [u8]) -> std::io::Result (*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 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 { @@ -292,23 +304,49 @@ impl Compositor { let _ = std::fs::remove_file(socket_path); let listener = UnixListener::bind(socket_path)?; - let runtime_dir = std::path::Path::new(socket_path).parent() + let runtime_dir = std::path::Path::new(socket_path) + .parent() .unwrap_or(std::path::Path::new("/tmp")); std::fs::write( runtime_dir.join("compositor.pid"), format!("{}\n", std::process::id()), - ).ok(); + ) + .ok(); let fb_size = (fb_height as usize) * (fb_stride as usize); let fb_data = map_framebuffer(fb_phys, fb_size); let globals = vec![ - Global { name: 1, interface: "wl_compositor".into(), version: 4 }, - Global { name: 2, interface: "wl_shm".into(), version: 1 }, - Global { name: 3, interface: "wl_shell".into(), version: 1 }, - Global { name: 4, interface: "wl_seat".into(), version: 5 }, - Global { name: 5, interface: "wl_output".into(), version: 3 }, - Global { name: 6, interface: "xdg_wm_base".into(), version: 1 }, + Global { + name: 1, + interface: "wl_compositor".into(), + version: 4, + }, + Global { + name: 2, + interface: "wl_shm".into(), + version: 1, + }, + Global { + name: 3, + interface: "wl_shell".into(), + version: 1, + }, + Global { + name: 4, + interface: "wl_seat".into(), + version: 5, + }, + Global { + name: 5, + interface: "wl_output".into(), + version: 3, + }, + Global { + name: 6, + interface: "xdg_wm_base".into(), + version: 1, + }, ]; Ok(Self { @@ -335,7 +373,8 @@ impl Compositor { pub fn run(&mut self) -> std::io::Result<()> { eprintln!("redbear-compositor: listening on Wayland socket"); let _ = std::fs::write( - std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".into()) + "/compositor.status", + std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".into()) + + "/compositor.status", "ready\n", ); for stream in self.listener.incoming() { @@ -343,13 +382,16 @@ impl Compositor { Ok(stream) => { let client_id = self.alloc_id(); eprintln!("redbear-compositor: client {} connected", client_id); - self.clients.lock().unwrap().insert(client_id, ClientState { - objects: HashMap::new(), - surfaces: HashMap::new(), - buffers: HashMap::new(), - shm_pools: HashMap::new(), - _next_id: 1, - }); + self.clients.lock().unwrap().insert( + client_id, + ClientState { + objects: HashMap::new(), + surfaces: HashMap::new(), + buffers: HashMap::new(), + shm_pools: HashMap::new(), + _next_id: 1, + }, + ); self.handle_client(client_id, stream); } Err(e) => eprintln!("redbear-compositor: accept error: {}", e), @@ -415,17 +457,30 @@ impl Compositor { ) -> 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]]); + 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 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)); + return Err(format!( + "malformed message: object={} opcode={} size={}", + object_id, opcode, msg_size + )); } - let payload = &data[offset+8..offset+msg_size]; + let payload = &data[offset + 8..offset + msg_size]; let object_type = if object_id == 1 { OBJECT_TYPE_WL_DISPLAY } else { @@ -436,18 +491,22 @@ impl Compositor { .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() }; + } else { + self.alloc_id() + }; self.send_callback_done(stream, callback_id, 0); } WL_DISPLAY_DELETE_ID => { if payload.len() >= 4 { - let obj_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let obj_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); let mut clients = self.clients.lock().unwrap(); if let Some(client) = clients.get_mut(&client_id) { client.objects.remove(&obj_id); @@ -459,7 +518,9 @@ impl Compositor { } WL_DISPLAY_GET_REGISTRY => { if payload.len() >= 4 { - let registry_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let registry_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); let mut clients = self.clients.lock().unwrap(); let mut send_globals = false; if let Some(client) = clients.get_mut(&client_id) { @@ -473,7 +534,10 @@ impl Compositor { } } _ => { - eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id); + eprintln!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); } }, OBJECT_TYPE_WL_REGISTRY => match opcode { @@ -484,69 +548,91 @@ impl Compositor { let _version = read_u32(payload, &mut cursor)?; let new_id = read_u32(payload, &mut cursor)?; - let mut clients = self.clients.lock().unwrap(); - if let Some(client) = clients.get_mut(&client_id) { - let type_id = match iface.as_str() { - "wl_compositor" => OBJECT_TYPE_WL_COMPOSITOR, - "wl_shm" => OBJECT_TYPE_WL_SHM, - "wl_shell" => OBJECT_TYPE_WL_SHELL, - "wl_seat" => OBJECT_TYPE_WL_SEAT, - "wl_output" => OBJECT_TYPE_WL_OUTPUT, - "xdg_wm_base" => OBJECT_TYPE_XDG_WM_BASE, - _ => 0, - }; - client.objects.insert(new_id, type_id); - if iface == "wl_shm" { - self.send_shm_format(stream, new_id, WL_SHM_FORMAT_ARGB8888); - self.send_shm_format(stream, new_id, WL_SHM_FORMAT_XRGB8888); - } - if iface == "wl_output" { - self.send_output_info(stream, new_id); - } - if iface == "wl_seat" { - self.send_seat_capabilities(stream, new_id); - } + let mut clients = self.clients.lock().unwrap(); + if let Some(client) = clients.get_mut(&client_id) { + let type_id = match iface.as_str() { + "wl_compositor" => OBJECT_TYPE_WL_COMPOSITOR, + "wl_shm" => OBJECT_TYPE_WL_SHM, + "wl_shell" => OBJECT_TYPE_WL_SHELL, + "wl_seat" => OBJECT_TYPE_WL_SEAT, + "wl_output" => OBJECT_TYPE_WL_OUTPUT, + "xdg_wm_base" => OBJECT_TYPE_XDG_WM_BASE, + _ => 0, + }; + client.objects.insert(new_id, type_id); + if iface == "wl_shm" { + self.send_shm_format(stream, new_id, WL_SHM_FORMAT_ARGB8888); + self.send_shm_format(stream, new_id, WL_SHM_FORMAT_XRGB8888); } + if iface == "wl_output" { + self.send_output_info(stream, new_id); + } + if iface == "wl_seat" { + self.send_seat_capabilities(stream, new_id); + } + } } _ => { - eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id); + eprintln!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); } }, OBJECT_TYPE_WL_COMPOSITOR => match opcode { WL_COMPOSITOR_CREATE_SURFACE => { if payload.len() >= 4 { - let surface_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let surface_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); let mut clients = self.clients.lock().unwrap(); if let Some(client) = clients.get_mut(&client_id) { client.objects.insert(surface_id, OBJECT_TYPE_WL_SURFACE); - client.surfaces.insert(surface_id, Surface { - buffer: None, - committed_buffer_id: None, - x: 0, y: 0, - _width: self.fb_width, - _height: self.fb_height, - }); + client.surfaces.insert( + surface_id, + Surface { + buffer: None, + committed_buffer_id: None, + x: 0, + y: 0, + _width: self.fb_width, + _height: self.fb_height, + }, + ); } } } _ => { - eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id); + eprintln!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); } }, OBJECT_TYPE_WL_SHM => match opcode { WL_SHM_CREATE_POOL => { if payload.len() >= 8 { - let pool_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); - let size = i32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]); - let fd_val = fds - .pop_front() - .ok_or_else(|| String::from("wl_shm.create_pool missing SCM_RIGHTS fd"))?; + let pool_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let size = i32::from_le_bytes([ + payload[4], payload[5], payload[6], payload[7], + ]); + let fd_val = fds.pop_front().ok_or_else(|| { + String::from("wl_shm.create_pool missing SCM_RIGHTS fd") + })?; if size > 0 { let file = unsafe { std::fs::File::from_raw_fd(fd_val) }; let mut clients = self.clients.lock().unwrap(); if let Some(client) = clients.get_mut(&client_id) { client.objects.insert(pool_id, OBJECT_TYPE_WL_SHM_POOL); - client.shm_pools.insert(pool_id, ShmPool { file, size: size as usize }); + client.shm_pools.insert( + pool_id, + ShmPool { + file, + size: size as usize, + }, + ); } } else { let _ = unsafe { libc::close(fd_val) }; @@ -554,54 +640,100 @@ impl Compositor { } } _ => { - eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id); + eprintln!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); } }, OBJECT_TYPE_WL_SHM_POOL => match opcode { WL_SHM_POOL_CREATE_BUFFER => { if payload.len() >= 20 { - let buffer_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); - let offset = u32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]); - let width = u32::from_le_bytes([payload[8], payload[9], payload[10], payload[11]]); - let height = u32::from_le_bytes([payload[12], payload[13], payload[14], payload[15]]); - let stride = u32::from_le_bytes([payload[16], payload[17], payload[18], payload[19]]); + let 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 }; + u32::from_le_bytes([ + payload[20], + payload[21], + payload[22], + payload[23], + ]) + } else { + WL_SHM_FORMAT_ARGB8888 + }; let mut clients = self.clients.lock().unwrap(); if let Some(client) = clients.get_mut(&client_id) { client.objects.insert(buffer_id, OBJECT_TYPE_WL_BUFFER); - client.buffers.insert(buffer_id, (object_id, Buffer { - pool_id: object_id, - offset, - width, - height, - stride, - _format: format, - })); + client.buffers.insert( + buffer_id, + ( + object_id, + Buffer { + pool_id: object_id, + offset, + width, + height, + stride, + _format: format, + }, + ), + ); } } } _ => { - eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id); + eprintln!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); } }, OBJECT_TYPE_WL_SURFACE => match opcode { WL_SURFACE_ATTACH => { if payload.len() >= 12 { - let buffer_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); - let _x = i32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]); - let _y = i32::from_le_bytes([payload[8], payload[9], payload[10], payload[11]]); + let buffer_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); + let _x = i32::from_le_bytes([ + payload[4], payload[5], payload[6], payload[7], + ]); + let _y = i32::from_le_bytes([ + payload[8], + payload[9], + payload[10], + payload[11], + ]); let mut clients = self.clients.lock().unwrap(); if let Some(client) = clients.get_mut(&client_id) { - if let Some((pool_id, buffer)) = client.buffers.get(&buffer_id).cloned() { + if let Some((pool_id, buffer)) = + client.buffers.get(&buffer_id).cloned() + { if let Some(surface) = client.surfaces.get_mut(&object_id) { - surface.buffer = Some(Buffer { - pool_id, - ..buffer - }); + surface.buffer = Some(Buffer { pool_id, ..buffer }); } } } @@ -613,22 +745,33 @@ impl Compositor { if let Some(client) = clients.get_mut(&client_id) { if let Some(surface) = client.surfaces.get_mut(&object_id) { let old_buffer = surface.committed_buffer_id.take(); - surface.committed_buffer_id = surface.buffer.as_ref().map(|b| { - client.buffers.iter() - .find(|(_, (_, buf))| buf.offset == b.offset && buf.width == b.width) - .map(|(id, _)| *id) - .unwrap_or(0) - }); + surface.committed_buffer_id = + surface.buffer.as_ref().map(|b| { + client + .buffers + .iter() + .find(|(_, (_, buf))| { + buf.offset == b.offset && buf.width == b.width + }) + .map(|(id, _)| *id) + .unwrap_or(0) + }); let surface_snapshot = surface.clone(); if let Some(ref buffer) = surface_snapshot.buffer { - if let Some(pool) = client.shm_pools.get_mut(&buffer.pool_id) { + if let Some(pool) = + client.shm_pools.get_mut(&buffer.pool_id) + { self.composite_buffer(pool, buffer, &surface_snapshot); } } old_buffer - } else { None } - } else { None } + } else { + None + } + } else { + None + } }; if let Some(buf_id) = release_id { @@ -641,13 +784,18 @@ impl Compositor { // No-op — we don't need damage tracking for a single-client greeter. } _ => { - eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id); + eprintln!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); } }, OBJECT_TYPE_WL_SHELL => match opcode { WL_SHELL_GET_SHELL_SURFACE => { if payload.len() >= 4 { - let new_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let new_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); let mut clients = self.clients.lock().unwrap(); if let Some(client) = clients.get_mut(&client_id) { client.objects.insert(new_id, OBJECT_TYPE_WL_SHELL_SURFACE); @@ -655,7 +803,10 @@ impl Compositor { } } _ => { - eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id); + eprintln!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); } }, OBJECT_TYPE_WL_SHELL_SURFACE => match opcode { @@ -663,13 +814,18 @@ impl Compositor { // No-op — we don't need window management for a single-client greeter. } _ => { - eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id); + eprintln!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); } }, OBJECT_TYPE_WL_SEAT => match opcode { WL_SEAT_GET_POINTER | WL_SEAT_GET_KEYBOARD => { if payload.len() >= 4 { - let new_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let 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, @@ -682,13 +838,18 @@ impl Compositor { } } _ => { - eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id); + eprintln!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); } }, OBJECT_TYPE_XDG_WM_BASE => match opcode { XDG_WM_BASE_GET_XDG_SURFACE => { if payload.len() >= 4 { - let new_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let new_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); let mut clients = self.clients.lock().unwrap(); if let Some(client) = clients.get_mut(&client_id) { client.objects.insert(new_id, OBJECT_TYPE_XDG_SURFACE); @@ -699,7 +860,10 @@ impl Compositor { // No-op — the greeter keeps the shell global alive for the client lifetime. } _ => { - eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id); + eprintln!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); } }, OBJECT_TYPE_XDG_SURFACE => match opcode { @@ -711,7 +875,9 @@ impl Compositor { } XDG_SURFACE_GET_TOPLEVEL => { if payload.len() >= 4 { - let toplevel_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let toplevel_id = u32::from_le_bytes([ + payload[0], payload[1], payload[2], payload[3], + ]); let mut clients = self.clients.lock().unwrap(); if let Some(client) = clients.get_mut(&client_id) { client.objects.insert(toplevel_id, OBJECT_TYPE_XDG_TOPLEVEL); @@ -726,7 +892,10 @@ impl Compositor { // Client acknowledged — ready for first commit. } _ => { - eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id); + eprintln!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); } }, OBJECT_TYPE_WL_OUTPUT @@ -734,10 +903,16 @@ impl Compositor { | OBJECT_TYPE_XDG_TOPLEVEL | OBJECT_TYPE_WL_POINTER | OBJECT_TYPE_WL_KEYBOARD => { - eprintln!("redbear-compositor: unhandled opcode {} on object {}", opcode, object_id); + eprintln!( + "redbear-compositor: unhandled opcode {} on object {}", + opcode, object_id + ); } _ => { - eprintln!("redbear-compositor: unhandled object {} opcode {}", object_id, opcode); + eprintln!( + "redbear-compositor: unhandled object {} opcode {}", + object_id, opcode + ); } } @@ -756,7 +931,10 @@ impl Compositor { } let mut src = vec![0u8; byte_count]; - if pool.file.seek(SeekFrom::Start(buffer.offset as u64)).is_err() + if pool + .file + .seek(SeekFrom::Start(buffer.offset as u64)) + .is_err() || pool.file.read_exact(&mut src).is_err() { return; @@ -856,7 +1034,8 @@ impl Compositor { fn main() { let wayland_display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".into()); - let runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp/run/redbear-greeter".into()); + let runtime_dir = + std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp/run/redbear-greeter".into()); let socket_path = format!("{}/{}", runtime_dir, wayland_display); // Read framebuffer parameters from environment (set by bootloader → vesad) @@ -872,10 +1051,9 @@ fn main() { .unwrap_or_else(|_| (fb_width * 4).to_string()) .parse() .unwrap_or(fb_width * 4); - let fb_phys_str = std::env::var("FRAMEBUFFER_ADDR") - .unwrap_or_else(|_| "0x80000000".into()); - let fb_phys = usize::from_str_radix(fb_phys_str.trim_start_matches("0x"), 16) - .unwrap_or(0x80000000); + let fb_phys_str = std::env::var("FRAMEBUFFER_ADDR").unwrap_or_else(|_| "0x80000000".into()); + let fb_phys = + usize::from_str_radix(fb_phys_str.trim_start_matches("0x"), 16).unwrap_or(0x80000000); eprintln!( "redbear-compositor: fb {}x{} stride {} phys 0x{:X}", @@ -893,9 +1071,9 @@ fn main() { eprintln!("redbear-compositor: failed to start: {}", e); } } - + let _ = std::fs::remove_file(&socket_path_clone); let _ = std::fs::remove_file( - std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".into()) + "/compositor.status" + std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".into()) + "/compositor.status", ); } diff --git a/local/recipes/wayland/redbear-compositor/source/tests/integration_test.rs b/local/recipes/wayland/redbear-compositor/source/tests/integration_test.rs index 3530c20a..bce1863b 100644 --- a/local/recipes/wayland/redbear-compositor/source/tests/integration_test.rs +++ b/local/recipes/wayland/redbear-compositor/source/tests/integration_test.rs @@ -1,11 +1,43 @@ // Integration test: verifies the compositor's Wayland protocol implementation // by starting a real compositor instance and connecting as a client. -use std::os::unix::net::UnixStream; use std::io::{Read, Write}; -use std::process::{Command, Child}; -use std::time::Duration; +use std::os::unix::net::UnixStream; +use std::process::{Child, Command}; use std::thread; +use std::time::Duration; + +fn push_wayland_string(buf: &mut Vec, value: &str) { + let bytes = value.as_bytes(); + buf.extend_from_slice(&((bytes.len() + 1) as u32).to_le_bytes()); + buf.extend_from_slice(bytes); + buf.push(0); + while buf.len() % 4 != 0 { + buf.push(0); + } +} + +fn read_wayland_string(payload: &[u8], cursor: &mut usize) -> String { + let length = u32::from_le_bytes([ + payload[*cursor], + payload[*cursor + 1], + payload[*cursor + 2], + payload[*cursor + 3], + ]) as usize; + *cursor += 4; + let bytes = &payload[*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]) + .unwrap() + .to_string() +} struct WaylandClient { stream: UnixStream, @@ -22,7 +54,10 @@ impl WaylandClient { } thread::sleep(Duration::from_millis(100)); } - Err(std::io::Error::new(std::io::ErrorKind::NotFound, "compositor socket did not appear")) + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "compositor socket did not appear", + )) } fn alloc_id(&mut self) -> u32 { @@ -34,9 +69,9 @@ impl WaylandClient { fn send_message(&mut self, object_id: u32, opcode: u16, payload: &[u8]) -> std::io::Result<()> { let size = 8 + payload.len(); let mut msg = Vec::with_capacity(size); - msg.extend_from_slice(&object_id.to_ne_bytes()); + msg.extend_from_slice(&object_id.to_le_bytes()); let header = ((size as u32) << 16) | opcode as u32; - msg.extend_from_slice(&header.to_ne_bytes()); + msg.extend_from_slice(&header.to_le_bytes()); msg.extend_from_slice(payload); self.stream.write_all(&msg) } @@ -44,8 +79,8 @@ impl WaylandClient { fn read_message(&mut self) -> std::io::Result<(u32, u16, Vec)> { let mut header = [0u8; 8]; self.stream.read_exact(&mut header)?; - let object_id = u32::from_ne_bytes([header[0], header[1], header[2], header[3]]); - let size_opcode = u32::from_ne_bytes([header[4], header[5], header[6], header[7]]); + let object_id = u32::from_le_bytes([header[0], header[1], header[2], header[3]]); + let size_opcode = u32::from_le_bytes([header[4], header[5], header[6], header[7]]); let size = ((size_opcode >> 16) & 0xFFFF) as usize; let opcode = (size_opcode & 0xFFFF) as u16; let mut payload = vec![0u8; size - 8]; @@ -57,25 +92,29 @@ impl WaylandClient { fn sync(&mut self) -> std::io::Result { let callback_id = self.alloc_id(); - self.send_message(1, 0, &callback_id.to_ne_bytes())?; // wl_display.sync + self.send_message(1, 0, &callback_id.to_le_bytes())?; // wl_display.sync Ok(callback_id) } fn get_registry(&mut self) -> std::io::Result { let registry_id = self.alloc_id(); - self.send_message(1, 1, ®istry_id.to_ne_bytes())?; // wl_display.get_registry + self.send_message(1, 1, ®istry_id.to_le_bytes())?; // wl_display.get_registry Ok(registry_id) } - fn bind(&mut self, registry_id: u32, name: u32, iface: &str, version: u32) -> std::io::Result { + fn bind( + &mut self, + registry_id: u32, + name: u32, + iface: &str, + version: u32, + ) -> std::io::Result { let new_id = self.alloc_id(); - let iface_bytes = iface.as_bytes(); - let mut payload = Vec::with_capacity(4 + iface_bytes.len() + 1 + 4 + 4); - payload.extend_from_slice(&name.to_ne_bytes()); - payload.extend_from_slice(iface_bytes); - payload.push(0); - payload.extend_from_slice(&version.to_ne_bytes()); - payload.extend_from_slice(&new_id.to_ne_bytes()); + let mut payload = Vec::new(); + payload.extend_from_slice(&name.to_le_bytes()); + push_wayland_string(&mut payload, iface); + payload.extend_from_slice(&version.to_le_bytes()); + payload.extend_from_slice(&new_id.to_le_bytes()); self.send_message(registry_id, 0, &payload)?; // wl_registry.bind Ok(new_id) } @@ -84,18 +123,21 @@ impl WaylandClient { fn start_compositor(socket_path: &str) -> Child { let compositor_bin = std::env::var("COMPOSITOR_BIN") .unwrap_or_else(|_| "target/debug/redbear-compositor".into()); - + let runtime_dir = std::path::Path::new(socket_path).parent().unwrap(); std::fs::create_dir_all(runtime_dir).ok(); - + let mut cmd = Command::new(&compositor_bin); - cmd.env("WAYLAND_DISPLAY", socket_path.rsplit('/').next().unwrap_or("wayland-0")) - .env("XDG_RUNTIME_DIR", runtime_dir) - .env("FRAMEBUFFER_WIDTH", "1280") - .env("FRAMEBUFFER_HEIGHT", "720") - .env("FRAMEBUFFER_STRIDE", "5120") - .env("FRAMEBUFFER_ADDR", "0x80000000"); - + cmd.env( + "WAYLAND_DISPLAY", + socket_path.rsplit('/').next().unwrap_or("wayland-0"), + ) + .env("XDG_RUNTIME_DIR", runtime_dir) + .env("FRAMEBUFFER_WIDTH", "1280") + .env("FRAMEBUFFER_HEIGHT", "720") + .env("FRAMEBUFFER_STRIDE", "5120") + .env("FRAMEBUFFER_ADDR", "0x80000000"); + cmd.spawn().expect("failed to start compositor") } @@ -103,24 +145,24 @@ fn start_compositor(socket_path: &str) -> Child { fn test_compositor_globals() { let socket = "/tmp/test-redbear-compositor.sock"; let _ = std::fs::remove_file(socket); - + let mut compositor = start_compositor(socket); - + let mut client = WaylandClient::connect(socket).expect("failed to connect"); - + // Get registry let _registry = client.get_registry().expect("get_registry failed"); - + // Read global events let mut globals = Vec::new(); for _ in 0..6 { match client.read_message() { Ok((_obj_id, opcode, payload)) => { assert_eq!(opcode, 0); // wl_registry.global - let name = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]); - let iface_end = payload[4..].iter().position(|&b| b == 0).unwrap_or(0); - let iface = std::str::from_utf8(&payload[4..4+iface_end]).unwrap(); - globals.push((name, iface.to_string())); + let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let mut cursor = 4; + let iface = read_wayland_string(&payload, &mut cursor); + globals.push((name, iface)); } Err(e) => { eprintln!("read error: {}", e); @@ -128,14 +170,29 @@ fn test_compositor_globals() { } } } - - assert!(globals.iter().any(|(_, i)| i == "wl_compositor"), "wl_compositor missing"); + + assert!( + globals.iter().any(|(_, i)| i == "wl_compositor"), + "wl_compositor missing" + ); assert!(globals.iter().any(|(_, i)| i == "wl_shm"), "wl_shm missing"); - assert!(globals.iter().any(|(_, i)| i == "wl_shell"), "wl_shell missing"); - assert!(globals.iter().any(|(_, i)| i == "wl_seat"), "wl_seat missing"); - assert!(globals.iter().any(|(_, i)| i == "wl_output"), "wl_output missing"); - assert!(globals.iter().any(|(_, i)| i == "xdg_wm_base"), "xdg_wm_base missing"); - + assert!( + globals.iter().any(|(_, i)| i == "wl_shell"), + "wl_shell missing" + ); + assert!( + globals.iter().any(|(_, i)| i == "wl_seat"), + "wl_seat missing" + ); + assert!( + globals.iter().any(|(_, i)| i == "wl_output"), + "wl_output missing" + ); + assert!( + globals.iter().any(|(_, i)| i == "xdg_wm_base"), + "xdg_wm_base missing" + ); + compositor.kill().ok(); let _ = std::fs::remove_file(socket); } @@ -144,43 +201,49 @@ fn test_compositor_globals() { fn test_compositor_shm_formats() { let socket = "/tmp/test-redbear-compositor-shm.sock"; let _ = std::fs::remove_file(socket); - + let mut compositor = start_compositor(socket); let mut client = WaylandClient::connect(socket).expect("failed to connect"); - + let registry = client.get_registry().expect("get_registry failed"); - + // Read globals to find wl_shm name let mut shm_name = 0u32; for _ in 0..6 { let (_, _, payload) = client.read_message().expect("read failed"); - let name = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]); - let iface_end = payload[4..].iter().position(|&b| b == 0).unwrap_or(0); - let iface = std::str::from_utf8(&payload[4..4+iface_end]).unwrap(); - if iface == "wl_shm" { shm_name = name; break; } + let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + let mut cursor = 4; + let iface = read_wayland_string(&payload, &mut cursor); + if iface == "wl_shm" { + shm_name = name; + break; + } } - + assert_ne!(shm_name, 0, "wl_shm global not found"); - + // Bind wl_shm - let _shm = client.bind(registry, shm_name, "wl_shm", 1).expect("bind shm failed"); - + let _shm = client + .bind(registry, shm_name, "wl_shm", 1) + .expect("bind shm failed"); + // Should receive format events let mut formats = Vec::new(); for _ in 0..3 { match client.read_message() { Ok((_, opcode, payload)) => { if opcode == 0 && payload.len() >= 4 { - let format = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]); + let format = + u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); formats.push(format); } } Err(_) => break, } } - + assert!(!formats.is_empty(), "no wl_shm.format events received"); - + compositor.kill().ok(); let _ = std::fs::remove_file(socket); } @@ -189,18 +252,18 @@ fn test_compositor_shm_formats() { fn test_compositor_sync_roundtrip() { let socket = "/tmp/test-redbear-compositor-sync.sock"; let _ = std::fs::remove_file(socket); - + let mut compositor = start_compositor(socket); let mut client = WaylandClient::connect(socket).expect("failed to connect"); - + let callback_id = client.sync().expect("sync failed"); - + // Should receive callback.done let (obj_id, opcode, payload) = client.read_message().expect("read failed"); assert_eq!(obj_id, callback_id, "callback id mismatch"); assert_eq!(opcode, 0, "expected callback.done (opcode 0)"); assert_eq!(payload.len(), 4, "callback.done payload should be 4 bytes"); - + compositor.kill().ok(); let _ = std::fs::remove_file(socket); }