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). #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.
# Full NewStuffQuick/NewStuffWidgets require Qt6Quick which is not yet QML-proven.
[source] [source]
tar = "https://invent.kde.org/frameworks/knewstuff/-/archive/v6.10.0/knewstuff-v6.10.0.tar.gz" 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" template = "custom"
dependencies = [ dependencies = [
"qtbase", "qtbase",
"qtdeclarative",
"kf6-extra-cmake-modules", "kf6-extra-cmake-modules",
"kf6-kcoreaddons", "kf6-kcoreaddons",
"kf6-ki18n", "kf6-ki18n",
"kf6-kconfig", "kf6-kconfig",
"kf6-karchive",
"kf6-kpackage",
] ]
script = """ script = """
DYNAMIC_INIT DYNAMIC_INIT
@@ -24,67 +24,40 @@ for qtdir in plugins mkspecs metatypes modules; do
fi fi
done done
BUILD_DIR="${COOKBOOK_SOURCE}/redox_build" sed -i 's/^include(ECMQmlModule)/# include(ECMQmlModule) # disabled for Redox core-only build/' \
mkdir -p "${BUILD_DIR}" "${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}" \ rm -f CMakeCache.txt
-DCMAKE_TOOLCHAIN_FILE="${COOKBOOK_SYSROOT}/usr/share/cmake/redox.cmake" \ rm -rf CMakeFiles
-DCMAKE_INSTALL_PREFIX="${COOKBOOK_STAGE}/usr" \
-DCMAKE_PREFIX_PATH="${COOKBOOK_STAGE}/usr;${COOKBOOK_SYSROOT}/usr;${HOST_BUILD}" \ cmake "${COOKBOOK_SOURCE}" \
-DBUILD_SHARED_LIBS=OFF \ -DCMAKE_TOOLCHAIN_FILE="${COOKBOOK_ROOT}/local/recipes/qt/redox-toolchain.cmake" \
-DBUILD_TESTING=OFF \ -DQT_HOST_PATH="${HOST_BUILD}" \
-DKF6_HOST_TOOLING="${HOST_BUILD}/lib/cmake" \ -DCMAKE_INSTALL_PREFIX=/usr \
-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 \
-DCMAKE_BUILD_TYPE=Release \ -DCMAKE_BUILD_TYPE=Release \
|| { -DCMAKE_PREFIX_PATH="${COOKBOOK_SYSROOT}:${COOKBOOK_STAGE}/usr/lib/cmake" \
echo "=== KNewStuff cmake configure failed falling back to stub configs ===" -DBUILD_TESTING=OFF \
STAGE="${COOKBOOK_STAGE}/usr" -DBUILD_QCH=OFF \
mkdir -p "${STAGE}/lib/cmake/KF6NewStuff" -DBUILD_DESIGNERPLUGIN=OFF \
cat > "${STAGE}/lib/cmake/KF6NewStuff/KF6NewStuffConfig.cmake" << 'EOFCFG' -Wno-dev
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
}
cmake --build "${BUILD_DIR}" -j "${COOKBOOK_MAKE_JOBS}" || { cmake --build . -j${COOKBOOK_MAKE_JOBS}
echo "=== KNewStuff build failed falling back to stub ===" cmake --install . --prefix "${COOKBOOK_STAGE}/usr"
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 --install "${BUILD_DIR}" for lib in "${COOKBOOK_STAGE}/usr/lib/"libKF6*.so.*; do
echo "=== KNewStuff real build (Core only, QML disabled) ===" [ -f "${lib}" ] || continue
patchelf --remove-rpath "${lib}" 2>/dev/null || true
done
""" """
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
# Build only C++ core, no QML modules # Build only the non-QML C++ core for now.
add_library(Kirigami STATIC) add_library(Kirigami)
add_library(KF6::Kirigami ALIAS Kirigami) add_library(KF6::Kirigami ALIAS Kirigami)
# Core C++ sources that don't require QML/QtQuick # 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. // Red Bear Compositor Runtime Check — verifies the compositor and greeter surface are healthy.
// Usage: redbear-compositor-check [--verbose] // 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::io::{Read, Write};
use std::mem;
use std::os::fd::AsRawFd;
use std::os::unix::net::UnixStream;
use std::time::Duration; use std::time::Duration;
fn check_wayland_socket() -> Result<(), String> { const WL_DISPLAY_SYNC: u16 = 0;
let runtime_dir = std::env::var("XDG_RUNTIME_DIR") const WL_DISPLAY_GET_REGISTRY: u16 = 1;
.unwrap_or_else(|_| "/tmp/run/redbear-greeter".into()); const WL_REGISTRY_BIND: u16 = 0;
let display = std::env::var("WAYLAND_DISPLAY") const WL_REGISTRY_GLOBAL: u16 = 0;
.unwrap_or_else(|_| "wayland-0".into()); const WL_COMPOSITOR_CREATE_SURFACE: u16 = 0;
let socket_path = format!("{}/{}", runtime_dir, display); 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() { fn push_u32(buf: &mut Vec<u8>, value: u32) {
return Err(format!("Wayland socket {} does not exist", socket_path)); 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) let value = u32::from_le_bytes([
.map_err(|e| format!("failed to connect to {}: {}", socket_path, e))?; data[*cursor],
stream.set_read_timeout(Some(Duration::from_secs(2))) data[*cursor + 1],
.map_err(|e| format!("failed to set timeout: {}", e))?; data[*cursor + 2],
data[*cursor + 3],
]);
*cursor += 4;
Ok(value)
}
// Send wl_display.sync request to verify protocol fn read_wayland_string(data: &[u8], cursor: &mut usize) -> Result<String, String> {
let display_id = 1u32; let length = read_u32(data, cursor)? as usize;
let callback_id = 2u32; if length == 0 {
let mut msg = Vec::new(); return Ok(String::new());
msg.extend_from_slice(&display_id.to_ne_bytes()); }
let size = 12u32; if *cursor + length > data.len() {
let opcode = 0u16; // wl_display.sync return Err(String::from("unexpected end of message while reading string"));
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))?;
// Read response let bytes = &data[*cursor..*cursor + length];
let mut buf = [0u8; 256]; let string_len = bytes.iter().position(|byte| *byte == 0).unwrap_or(bytes.len());
let n = stream.read(&mut buf) *cursor += length;
.map_err(|e| format!("read failed: {}", e))?; while *cursor % 4 != 0 {
*cursor += 1;
}
if n < 8 { std::str::from_utf8(&bytes[..string_len])
return Err(format!("short response: {} bytes", n)); .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(()) 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>> { fn check_binaries() -> Result<(), Vec<String>> {
let mut missing = Vec::new(); let mut missing = Vec::new();
for bin in &["/usr/bin/redbear-compositor", "/usr/bin/redbear-greeterd", for bin in &[
"/usr/bin/redbear-greeter-ui", "/usr/bin/redbear-authd", "/usr/bin/redbear-compositor",
"/usr/bin/kwin_wayland_wrapper"] { "/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() { if !std::path::Path::new(bin).exists() {
missing.push(bin.to_string()); 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> { fn check_framebuffer() -> Result<(), String> {
@@ -89,7 +456,11 @@ fn check_services() -> Result<(), Vec<String>> {
issues.push(format!("{} not found at {}", name, path)); 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() { fn main() {
@@ -113,7 +484,9 @@ fn main() {
($label:expr, $check:expr, vec) => { ($label:expr, $check:expr, vec) => {
match $check { match $check {
Ok(()) => { Ok(()) => {
if verbose { println!(" PASS {}", $label); } if verbose {
println!(" PASS {}", $label);
}
} }
Err(errs) => { Err(errs) => {
for e in errs { for e in errs {
@@ -143,14 +516,17 @@ fn main() {
check!("runtime services", check_services(), vec); check!("runtime services", check_services(), vec);
if verbose { 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 { if exit == 0 {
println!("redbear-compositor-check: all checks passed"); println!("redbear-compositor-check: all checks passed");
} else { } 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); std::process::exit(1);
} }
} }
@@ -20,7 +20,10 @@ use std::io::{Read, Seek, SeekFrom, Write};
use std::mem; use std::mem;
use std::os::fd::{AsRawFd, FromRawFd, RawFd}; use std::os::fd::{AsRawFd, FromRawFd, RawFd};
use std::os::unix::net::{UnixListener, UnixStream}; 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> { fn map_framebuffer(_phys: usize, size: usize) -> Vec<u8> {
vec![0u8; size] vec![0u8; size]
@@ -75,11 +78,16 @@ fn read_wayland_string(data: &[u8], cursor: &mut usize) -> Result<String, String
return Ok(String::new()); return Ok(String::new());
} }
if *cursor + length > data.len() { 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 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; *cursor += length;
while *cursor % 4 != 0 { while *cursor % 4 != 0 {
*cursor += 1; *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}")) .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 { let mut iov = libc::iovec {
iov_base: data.as_mut_ptr().cast(), iov_base: data.as_mut_ptr().cast(),
iov_len: data.len(), 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 (*cmsg).cmsg_level == libc::SOL_SOCKET && (*cmsg).cmsg_type == libc::SCM_RIGHTS
}; };
if is_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 fd_count = data_len / mem::size_of::<RawFd>();
let data_ptr = unsafe { libc::CMSG_DATA(cmsg).cast::<RawFd>() }; let data_ptr = unsafe { libc::CMSG_DATA(cmsg).cast::<RawFd>() };
for index in 0..fd_count { for index in 0..fd_count {
@@ -292,23 +304,49 @@ impl Compositor {
let _ = std::fs::remove_file(socket_path); let _ = std::fs::remove_file(socket_path);
let listener = UnixListener::bind(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")); .unwrap_or(std::path::Path::new("/tmp"));
std::fs::write( std::fs::write(
runtime_dir.join("compositor.pid"), runtime_dir.join("compositor.pid"),
format!("{}\n", std::process::id()), format!("{}\n", std::process::id()),
).ok(); )
.ok();
let fb_size = (fb_height as usize) * (fb_stride as usize); let fb_size = (fb_height as usize) * (fb_stride as usize);
let fb_data = map_framebuffer(fb_phys, fb_size); let fb_data = map_framebuffer(fb_phys, fb_size);
let globals = vec![ let globals = vec![
Global { name: 1, interface: "wl_compositor".into(), version: 4 }, Global {
Global { name: 2, interface: "wl_shm".into(), version: 1 }, name: 1,
Global { name: 3, interface: "wl_shell".into(), version: 1 }, interface: "wl_compositor".into(),
Global { name: 4, interface: "wl_seat".into(), version: 5 }, version: 4,
Global { name: 5, interface: "wl_output".into(), version: 3 }, },
Global { name: 6, interface: "xdg_wm_base".into(), version: 1 }, 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 { Ok(Self {
@@ -335,7 +373,8 @@ impl Compositor {
pub fn run(&mut self) -> std::io::Result<()> { pub fn run(&mut self) -> std::io::Result<()> {
eprintln!("redbear-compositor: listening on Wayland socket"); eprintln!("redbear-compositor: listening on Wayland socket");
let _ = std::fs::write( 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", "ready\n",
); );
for stream in self.listener.incoming() { for stream in self.listener.incoming() {
@@ -343,13 +382,16 @@ impl Compositor {
Ok(stream) => { Ok(stream) => {
let client_id = self.alloc_id(); let client_id = self.alloc_id();
eprintln!("redbear-compositor: client {} connected", client_id); eprintln!("redbear-compositor: client {} connected", client_id);
self.clients.lock().unwrap().insert(client_id, ClientState { self.clients.lock().unwrap().insert(
objects: HashMap::new(), client_id,
surfaces: HashMap::new(), ClientState {
buffers: HashMap::new(), objects: HashMap::new(),
shm_pools: HashMap::new(), surfaces: HashMap::new(),
_next_id: 1, buffers: HashMap::new(),
}); shm_pools: HashMap::new(),
_next_id: 1,
},
);
self.handle_client(client_id, stream); self.handle_client(client_id, stream);
} }
Err(e) => eprintln!("redbear-compositor: accept error: {}", e), Err(e) => eprintln!("redbear-compositor: accept error: {}", e),
@@ -415,17 +457,30 @@ impl Compositor {
) -> Result<(), String> { ) -> Result<(), String> {
let mut offset = 0; let mut offset = 0;
while offset + 8 <= data.len() { 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] // 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 msg_size = ((size_opcode >> 16) & 0xFFFF) as usize;
let opcode = (size_opcode & 0xFFFF) as u16; let opcode = (size_opcode & 0xFFFF) as u16;
if msg_size < 8 || offset + msg_size > data.len() { 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 { let object_type = if object_id == 1 {
OBJECT_TYPE_WL_DISPLAY OBJECT_TYPE_WL_DISPLAY
} else { } else {
@@ -442,12 +497,16 @@ impl Compositor {
WL_DISPLAY_SYNC => { WL_DISPLAY_SYNC => {
let callback_id = if payload.len() >= 4 { let callback_id = if payload.len() >= 4 {
u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]) 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); self.send_callback_done(stream, callback_id, 0);
} }
WL_DISPLAY_DELETE_ID => { WL_DISPLAY_DELETE_ID => {
if payload.len() >= 4 { 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(); let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) { if let Some(client) = clients.get_mut(&client_id) {
client.objects.remove(&obj_id); client.objects.remove(&obj_id);
@@ -459,7 +518,9 @@ impl Compositor {
} }
WL_DISPLAY_GET_REGISTRY => { WL_DISPLAY_GET_REGISTRY => {
if payload.len() >= 4 { 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 clients = self.clients.lock().unwrap();
let mut send_globals = false; let mut send_globals = false;
if let Some(client) = clients.get_mut(&client_id) { 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 { OBJECT_TYPE_WL_REGISTRY => match opcode {
@@ -484,69 +548,91 @@ impl Compositor {
let _version = read_u32(payload, &mut cursor)?; let _version = read_u32(payload, &mut cursor)?;
let new_id = read_u32(payload, &mut cursor)?; let new_id = read_u32(payload, &mut cursor)?;
let mut clients = self.clients.lock().unwrap(); let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) { if let Some(client) = clients.get_mut(&client_id) {
let type_id = match iface.as_str() { let type_id = match iface.as_str() {
"wl_compositor" => OBJECT_TYPE_WL_COMPOSITOR, "wl_compositor" => OBJECT_TYPE_WL_COMPOSITOR,
"wl_shm" => OBJECT_TYPE_WL_SHM, "wl_shm" => OBJECT_TYPE_WL_SHM,
"wl_shell" => OBJECT_TYPE_WL_SHELL, "wl_shell" => OBJECT_TYPE_WL_SHELL,
"wl_seat" => OBJECT_TYPE_WL_SEAT, "wl_seat" => OBJECT_TYPE_WL_SEAT,
"wl_output" => OBJECT_TYPE_WL_OUTPUT, "wl_output" => OBJECT_TYPE_WL_OUTPUT,
"xdg_wm_base" => OBJECT_TYPE_XDG_WM_BASE, "xdg_wm_base" => OBJECT_TYPE_XDG_WM_BASE,
_ => 0, _ => 0,
}; };
client.objects.insert(new_id, type_id); client.objects.insert(new_id, type_id);
if iface == "wl_shm" { 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_ARGB8888);
self.send_shm_format(stream, new_id, WL_SHM_FORMAT_XRGB8888); 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);
}
} }
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 { OBJECT_TYPE_WL_COMPOSITOR => match opcode {
WL_COMPOSITOR_CREATE_SURFACE => { WL_COMPOSITOR_CREATE_SURFACE => {
if payload.len() >= 4 { 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(); let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) { if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(surface_id, OBJECT_TYPE_WL_SURFACE); client.objects.insert(surface_id, OBJECT_TYPE_WL_SURFACE);
client.surfaces.insert(surface_id, Surface { client.surfaces.insert(
buffer: None, surface_id,
committed_buffer_id: None, Surface {
x: 0, y: 0, buffer: None,
_width: self.fb_width, committed_buffer_id: None,
_height: self.fb_height, 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 { OBJECT_TYPE_WL_SHM => match opcode {
WL_SHM_CREATE_POOL => { WL_SHM_CREATE_POOL => {
if payload.len() >= 8 { if payload.len() >= 8 {
let pool_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); let pool_id = u32::from_le_bytes([
let size = i32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]); payload[0], payload[1], payload[2], payload[3],
let fd_val = fds ]);
.pop_front() let size = i32::from_le_bytes([
.ok_or_else(|| String::from("wl_shm.create_pool missing SCM_RIGHTS fd"))?; 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 { if size > 0 {
let file = unsafe { std::fs::File::from_raw_fd(fd_val) }; let file = unsafe { std::fs::File::from_raw_fd(fd_val) };
let mut clients = self.clients.lock().unwrap(); let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) { if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(pool_id, OBJECT_TYPE_WL_SHM_POOL); 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 { } else {
let _ = unsafe { libc::close(fd_val) }; 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 { OBJECT_TYPE_WL_SHM_POOL => match opcode {
WL_SHM_POOL_CREATE_BUFFER => { WL_SHM_POOL_CREATE_BUFFER => {
if payload.len() >= 20 { if payload.len() >= 20 {
let buffer_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); let buffer_id = u32::from_le_bytes([
let offset = u32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]); payload[0], payload[1], payload[2], payload[3],
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 offset = u32::from_le_bytes([
let stride = u32::from_le_bytes([payload[16], payload[17], payload[18], payload[19]]); 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 { let format = if payload.len() >= 24 {
u32::from_le_bytes([payload[20], payload[21], payload[22], payload[23]]) u32::from_le_bytes([
} else { WL_SHM_FORMAT_ARGB8888 }; payload[20],
payload[21],
payload[22],
payload[23],
])
} else {
WL_SHM_FORMAT_ARGB8888
};
let mut clients = self.clients.lock().unwrap(); let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) { if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(buffer_id, OBJECT_TYPE_WL_BUFFER); client.objects.insert(buffer_id, OBJECT_TYPE_WL_BUFFER);
client.buffers.insert(buffer_id, (object_id, Buffer { client.buffers.insert(
pool_id: object_id, buffer_id,
offset, (
width, object_id,
height, Buffer {
stride, pool_id: object_id,
_format: format, 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 { OBJECT_TYPE_WL_SURFACE => match opcode {
WL_SURFACE_ATTACH => { WL_SURFACE_ATTACH => {
if payload.len() >= 12 { if payload.len() >= 12 {
let buffer_id = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); let buffer_id = u32::from_le_bytes([
let _x = i32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]); payload[0], payload[1], payload[2], payload[3],
let _y = i32::from_le_bytes([payload[8], payload[9], payload[10], payload[11]]); ]);
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(); let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) { 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) { if let Some(surface) = client.surfaces.get_mut(&object_id) {
surface.buffer = Some(Buffer { surface.buffer = Some(Buffer { pool_id, ..buffer });
pool_id,
..buffer
});
} }
} }
} }
@@ -613,22 +745,33 @@ impl Compositor {
if let Some(client) = clients.get_mut(&client_id) { if let Some(client) = clients.get_mut(&client_id) {
if let Some(surface) = client.surfaces.get_mut(&object_id) { if let Some(surface) = client.surfaces.get_mut(&object_id) {
let old_buffer = surface.committed_buffer_id.take(); let old_buffer = surface.committed_buffer_id.take();
surface.committed_buffer_id = surface.buffer.as_ref().map(|b| { surface.committed_buffer_id =
client.buffers.iter() surface.buffer.as_ref().map(|b| {
.find(|(_, (_, buf))| buf.offset == b.offset && buf.width == b.width) client
.map(|(id, _)| *id) .buffers
.unwrap_or(0) .iter()
}); .find(|(_, (_, buf))| {
buf.offset == b.offset && buf.width == b.width
})
.map(|(id, _)| *id)
.unwrap_or(0)
});
let surface_snapshot = surface.clone(); let surface_snapshot = surface.clone();
if let Some(ref buffer) = surface_snapshot.buffer { 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); self.composite_buffer(pool, buffer, &surface_snapshot);
} }
} }
old_buffer old_buffer
} else { None } } else {
} else { None } None
}
} else {
None
}
}; };
if let Some(buf_id) = release_id { 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. // 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 { OBJECT_TYPE_WL_SHELL => match opcode {
WL_SHELL_GET_SHELL_SURFACE => { WL_SHELL_GET_SHELL_SURFACE => {
if payload.len() >= 4 { 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(); let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) { if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(new_id, OBJECT_TYPE_WL_SHELL_SURFACE); 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 { 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. // 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 { OBJECT_TYPE_WL_SEAT => match opcode {
WL_SEAT_GET_POINTER | WL_SEAT_GET_KEYBOARD => { WL_SEAT_GET_POINTER | WL_SEAT_GET_KEYBOARD => {
if payload.len() >= 4 { 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 { let object_type = match opcode {
WL_SEAT_GET_POINTER => OBJECT_TYPE_WL_POINTER, WL_SEAT_GET_POINTER => OBJECT_TYPE_WL_POINTER,
WL_SEAT_GET_KEYBOARD => OBJECT_TYPE_WL_KEYBOARD, 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 { OBJECT_TYPE_XDG_WM_BASE => match opcode {
XDG_WM_BASE_GET_XDG_SURFACE => { XDG_WM_BASE_GET_XDG_SURFACE => {
if payload.len() >= 4 { 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(); let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) { if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(new_id, OBJECT_TYPE_XDG_SURFACE); 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. // 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 { OBJECT_TYPE_XDG_SURFACE => match opcode {
@@ -711,7 +875,9 @@ impl Compositor {
} }
XDG_SURFACE_GET_TOPLEVEL => { XDG_SURFACE_GET_TOPLEVEL => {
if payload.len() >= 4 { 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(); let mut clients = self.clients.lock().unwrap();
if let Some(client) = clients.get_mut(&client_id) { if let Some(client) = clients.get_mut(&client_id) {
client.objects.insert(toplevel_id, OBJECT_TYPE_XDG_TOPLEVEL); client.objects.insert(toplevel_id, OBJECT_TYPE_XDG_TOPLEVEL);
@@ -726,7 +892,10 @@ impl Compositor {
// Client acknowledged — ready for first commit. // 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 OBJECT_TYPE_WL_OUTPUT
@@ -734,10 +903,16 @@ impl Compositor {
| OBJECT_TYPE_XDG_TOPLEVEL | OBJECT_TYPE_XDG_TOPLEVEL
| OBJECT_TYPE_WL_POINTER | OBJECT_TYPE_WL_POINTER
| OBJECT_TYPE_WL_KEYBOARD => { | 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]; 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() || pool.file.read_exact(&mut src).is_err()
{ {
return; return;
@@ -856,7 +1034,8 @@ impl Compositor {
fn main() { fn main() {
let wayland_display = std::env::var("WAYLAND_DISPLAY").unwrap_or_else(|_| "wayland-0".into()); 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); let socket_path = format!("{}/{}", runtime_dir, wayland_display);
// Read framebuffer parameters from environment (set by bootloader → vesad) // Read framebuffer parameters from environment (set by bootloader → vesad)
@@ -872,10 +1051,9 @@ fn main() {
.unwrap_or_else(|_| (fb_width * 4).to_string()) .unwrap_or_else(|_| (fb_width * 4).to_string())
.parse() .parse()
.unwrap_or(fb_width * 4); .unwrap_or(fb_width * 4);
let fb_phys_str = std::env::var("FRAMEBUFFER_ADDR") let fb_phys_str = std::env::var("FRAMEBUFFER_ADDR").unwrap_or_else(|_| "0x80000000".into());
.unwrap_or_else(|_| "0x80000000".into()); let fb_phys =
let fb_phys = usize::from_str_radix(fb_phys_str.trim_start_matches("0x"), 16) usize::from_str_radix(fb_phys_str.trim_start_matches("0x"), 16).unwrap_or(0x80000000);
.unwrap_or(0x80000000);
eprintln!( eprintln!(
"redbear-compositor: fb {}x{} stride {} phys 0x{:X}", "redbear-compositor: fb {}x{} stride {} phys 0x{:X}",
@@ -896,6 +1074,6 @@ fn main() {
let _ = std::fs::remove_file(&socket_path_clone); let _ = std::fs::remove_file(&socket_path_clone);
let _ = std::fs::remove_file( 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 // Integration test: verifies the compositor's Wayland protocol implementation
// by starting a real compositor instance and connecting as a client. // by starting a real compositor instance and connecting as a client.
use std::os::unix::net::UnixStream;
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::process::{Command, Child}; use std::os::unix::net::UnixStream;
use std::time::Duration; use std::process::{Child, Command};
use std::thread; 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 { struct WaylandClient {
stream: UnixStream, stream: UnixStream,
@@ -22,7 +54,10 @@ impl WaylandClient {
} }
thread::sleep(Duration::from_millis(100)); 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 { 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<()> { fn send_message(&mut self, object_id: u32, opcode: u16, payload: &[u8]) -> std::io::Result<()> {
let size = 8 + payload.len(); let size = 8 + payload.len();
let mut msg = Vec::with_capacity(size); 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; 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); msg.extend_from_slice(payload);
self.stream.write_all(&msg) self.stream.write_all(&msg)
} }
@@ -44,8 +79,8 @@ impl WaylandClient {
fn read_message(&mut self) -> std::io::Result<(u32, u16, Vec<u8>)> { fn read_message(&mut self) -> std::io::Result<(u32, u16, Vec<u8>)> {
let mut header = [0u8; 8]; let mut header = [0u8; 8];
self.stream.read_exact(&mut header)?; self.stream.read_exact(&mut header)?;
let object_id = u32::from_ne_bytes([header[0], header[1], header[2], header[3]]); let object_id = u32::from_le_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 size_opcode = u32::from_le_bytes([header[4], header[5], header[6], header[7]]);
let size = ((size_opcode >> 16) & 0xFFFF) as usize; let size = ((size_opcode >> 16) & 0xFFFF) as usize;
let opcode = (size_opcode & 0xFFFF) as u16; let opcode = (size_opcode & 0xFFFF) as u16;
let mut payload = vec![0u8; size - 8]; let mut payload = vec![0u8; size - 8];
@@ -57,25 +92,29 @@ impl WaylandClient {
fn sync(&mut self) -> std::io::Result<u32> { fn sync(&mut self) -> std::io::Result<u32> {
let callback_id = self.alloc_id(); 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) Ok(callback_id)
} }
fn get_registry(&mut self) -> std::io::Result<u32> { fn get_registry(&mut self) -> std::io::Result<u32> {
let registry_id = self.alloc_id(); 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) 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 new_id = self.alloc_id();
let iface_bytes = iface.as_bytes(); let mut payload = Vec::new();
let mut payload = Vec::with_capacity(4 + iface_bytes.len() + 1 + 4 + 4); payload.extend_from_slice(&name.to_le_bytes());
payload.extend_from_slice(&name.to_ne_bytes()); push_wayland_string(&mut payload, iface);
payload.extend_from_slice(iface_bytes); payload.extend_from_slice(&version.to_le_bytes());
payload.push(0); payload.extend_from_slice(&new_id.to_le_bytes());
payload.extend_from_slice(&version.to_ne_bytes());
payload.extend_from_slice(&new_id.to_ne_bytes());
self.send_message(registry_id, 0, &payload)?; // wl_registry.bind self.send_message(registry_id, 0, &payload)?; // wl_registry.bind
Ok(new_id) Ok(new_id)
} }
@@ -89,12 +128,15 @@ fn start_compositor(socket_path: &str) -> Child {
std::fs::create_dir_all(runtime_dir).ok(); std::fs::create_dir_all(runtime_dir).ok();
let mut cmd = Command::new(&compositor_bin); let mut cmd = Command::new(&compositor_bin);
cmd.env("WAYLAND_DISPLAY", socket_path.rsplit('/').next().unwrap_or("wayland-0")) cmd.env(
.env("XDG_RUNTIME_DIR", runtime_dir) "WAYLAND_DISPLAY",
.env("FRAMEBUFFER_WIDTH", "1280") socket_path.rsplit('/').next().unwrap_or("wayland-0"),
.env("FRAMEBUFFER_HEIGHT", "720") )
.env("FRAMEBUFFER_STRIDE", "5120") .env("XDG_RUNTIME_DIR", runtime_dir)
.env("FRAMEBUFFER_ADDR", "0x80000000"); .env("FRAMEBUFFER_WIDTH", "1280")
.env("FRAMEBUFFER_HEIGHT", "720")
.env("FRAMEBUFFER_STRIDE", "5120")
.env("FRAMEBUFFER_ADDR", "0x80000000");
cmd.spawn().expect("failed to start compositor") cmd.spawn().expect("failed to start compositor")
} }
@@ -117,10 +159,10 @@ fn test_compositor_globals() {
match client.read_message() { match client.read_message() {
Ok((_obj_id, opcode, payload)) => { Ok((_obj_id, opcode, payload)) => {
assert_eq!(opcode, 0); // wl_registry.global assert_eq!(opcode, 0); // wl_registry.global
let name = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]); let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
let iface_end = payload[4..].iter().position(|&b| b == 0).unwrap_or(0); let mut cursor = 4;
let iface = std::str::from_utf8(&payload[4..4+iface_end]).unwrap(); let iface = read_wayland_string(&payload, &mut cursor);
globals.push((name, iface.to_string())); globals.push((name, iface));
} }
Err(e) => { Err(e) => {
eprintln!("read error: {}", e); eprintln!("read error: {}", e);
@@ -129,12 +171,27 @@ 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_shm"), "wl_shm missing");
assert!(globals.iter().any(|(_, i)| i == "wl_shell"), "wl_shell missing"); assert!(
assert!(globals.iter().any(|(_, i)| i == "wl_seat"), "wl_seat missing"); globals.iter().any(|(_, i)| i == "wl_shell"),
assert!(globals.iter().any(|(_, i)| i == "wl_output"), "wl_output missing"); "wl_shell missing"
assert!(globals.iter().any(|(_, i)| i == "xdg_wm_base"), "xdg_wm_base 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(); compositor.kill().ok();
let _ = std::fs::remove_file(socket); let _ = std::fs::remove_file(socket);
@@ -154,16 +211,21 @@ fn test_compositor_shm_formats() {
let mut shm_name = 0u32; let mut shm_name = 0u32;
for _ in 0..6 { for _ in 0..6 {
let (_, _, payload) = client.read_message().expect("read failed"); let (_, _, payload) = client.read_message().expect("read failed");
let name = u32::from_ne_bytes([payload[0], payload[1], payload[2], payload[3]]); let name = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]);
let iface_end = payload[4..].iter().position(|&b| b == 0).unwrap_or(0); let mut cursor = 4;
let iface = std::str::from_utf8(&payload[4..4+iface_end]).unwrap(); let iface = read_wayland_string(&payload, &mut cursor);
if iface == "wl_shm" { shm_name = name; break; } if iface == "wl_shm" {
shm_name = name;
break;
}
} }
assert_ne!(shm_name, 0, "wl_shm global not found"); assert_ne!(shm_name, 0, "wl_shm global not found");
// Bind wl_shm // 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 // Should receive format events
let mut formats = Vec::new(); let mut formats = Vec::new();
@@ -171,7 +233,8 @@ fn test_compositor_shm_formats() {
match client.read_message() { match client.read_message() {
Ok((_, opcode, payload)) => { Ok((_, opcode, payload)) => {
if opcode == 0 && payload.len() >= 4 { 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); formats.push(format);
} }
} }