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
This commit is contained in:
2026-04-29 14:53:09 +01:00
parent edb68153e3
commit 68e4531a45
5 changed files with 876 additions and 286 deletions
+35 -62
View File
@@ -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
"""
@@ -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
@@ -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<u8>, value: u32) {
buf.extend_from_slice(&value.to_le_bytes());
}
fn push_i32(buf: &mut Vec<u8>, value: i32) {
buf.extend_from_slice(&value.to_le_bytes());
}
fn push_wayland_string(buf: &mut Vec<u8>, value: &str) {
let bytes = value.as_bytes();
push_u32(buf, (bytes.len() + 1) as u32);
buf.extend_from_slice(bytes);
buf.push(0);
while buf.len() % 4 != 0 {
buf.push(0);
}
}
fn read_u32(data: &[u8], cursor: &mut usize) -> Result<u32, String> {
if *cursor + 4 > data.len() {
return Err(String::from("unexpected end of message while reading u32"));
}
let 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<String, String> {
let length = read_u32(data, cursor)? as usize;
if length == 0 {
return Ok(String::new());
}
if *cursor + length > data.len() {
return Err(String::from("unexpected end of message while reading string"));
}
// 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<Self, String> {
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::<i32>()) 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::<i32>()) as u32) as _;
std::ptr::copy_nonoverlapping(
fds.as_ptr().cast::<u8>(),
libc::CMSG_DATA(cmsg).cast::<u8>(),
fds.len() * mem::size_of::<i32>(),
);
}
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<u8>), 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<u32, String> {
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<u32, String> {
let registry_id = self.alloc_id();
self.send_message(1, WL_DISPLAY_GET_REGISTRY, &registry_id.to_le_bytes())?;
Ok(registry_id)
}
fn bind(&mut self, registry_id: u32, name: u32, interface: &str, version: u32) -> Result<u32, String> {
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<HashMap<String, u32>, 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<u32, String> {
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<String>> {
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<String>> {
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);
}
}
@@ -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<u8> {
vec![0u8; size]
@@ -75,11 +78,16 @@ fn read_wayland_string(data: &[u8], cursor: &mut usize) -> Result<String, String
return Ok(String::new());
}
if *cursor + length > 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<String, String
.map_err(|err| format!("invalid UTF-8 in Wayland string: {err}"))
}
fn recv_with_rights(stream: &mut UnixStream, data: &mut [u8]) -> std::io::Result<(usize, VecDeque<RawFd>)> {
fn recv_with_rights(
stream: &mut UnixStream,
data: &mut [u8],
) -> std::io::Result<(usize, VecDeque<RawFd>)> {
let mut iov = libc::iovec {
iov_base: data.as_mut_ptr().cast(),
iov_len: data.len(),
@@ -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::<libc::cmsghdr>());
let data_len = unsafe { (*cmsg).cmsg_len as usize }
.saturating_sub(mem::size_of::<libc::cmsghdr>());
let fd_count = data_len / mem::size_of::<RawFd>();
let data_ptr = unsafe { libc::CMSG_DATA(cmsg).cast::<RawFd>() };
for index in 0..fd_count {
@@ -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",
);
}
@@ -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<u8>, 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<u8>)> {
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<u32> {
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<u32> {
let registry_id = self.alloc_id();
self.send_message(1, 1, &registry_id.to_ne_bytes())?; // wl_display.get_registry
self.send_message(1, 1, &registry_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<u32> {
fn bind(
&mut self,
registry_id: u32,
name: u32,
iface: &str,
version: u32,
) -> std::io::Result<u32> {
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);
}