milestone: Phase 4-5 completion + KF6 honesty + KDE session + GPU CS ioctl

Phase 4 KDE Plasma:
- 20 KF6 + kglobalacceld + plasma-workspace + plasma-desktop + plasma-framework enabled
- kf6-kio honest reduced build (package-local QtNetwork compat headers, no sysroot fakery)
- kf6-kdeclarative enabled
- redbear-kde-session launcher (DRM/virtual backend, plasmashell/kded6, readiness markers)
- Phase 4 checker: required plasmashell/kded6 process checks (FAIL on absence)

Phase 5 Hardware GPU:
- CS ioctl checker (GEM allocation, PRIME sharing, private CS submit/wait over /scheme/drm/card0)
- Enhanced GPU checker with hardware rendering readiness summary
- test-phase5-cs-runtime.sh harness

Qt6Quick honesty: qtdeclarative exports Qt6Quick metadata; downstream QML/Kirigami/KWin proof still insufficient.
Oracle-verified: Phase 4-5 (5 rounds).

Build: zero warnings.
This commit is contained in:
2026-04-29 11:05:22 +01:00
parent 8acc73d774
commit 2fdb7906f8
20 changed files with 2444 additions and 891 deletions
@@ -37,6 +37,8 @@ mkdir -pv "$COOKBOOK_STAGE/usr/bin"
mkdir -pv "$COOKBOOK_STAGE/usr/share/redbear/greeter"
cp -v "$COOKBOOK_SOURCE/redbear-greeter-compositor" "$COOKBOOK_STAGE/usr/share/redbear/greeter/redbear-greeter-compositor"
chmod 0755 "$COOKBOOK_STAGE/usr/share/redbear/greeter/redbear-greeter-compositor"
cp -v "$COOKBOOK_SOURCE/redbear-kde-session" "$COOKBOOK_STAGE/usr/bin/redbear-kde-session"
chmod 0755 "$COOKBOOK_STAGE/usr/bin/redbear-kde-session"
cp -v "$COOKBOOK_RECIPE/../../../../local/Assets/images/Red Bear OS loading background.png" "$COOKBOOK_STAGE/usr/share/redbear/greeter/background.png"
cp -v "$COOKBOOK_RECIPE/../../../../local/Assets/images/Red Bear OS icon.png" "$COOKBOOK_STAGE/usr/share/redbear/greeter/icon.png"
cp -v "$COOKBOOK_SOURCE/redbear-greeter-compositor" "$COOKBOOK_STAGE/usr/bin/redbear-greeter-compositor"
@@ -0,0 +1,244 @@
#!/usr/bin/env bash
set -euo pipefail
VALIDATION_REQUEST="/run/redbear-kde-session.validation-request"
VALIDATION_SUCCESS="/run/redbear-kde-session.validation-success"
kwin_pid=""
optional_pids=()
export DESKTOP_SESSION="${DESKTOP_SESSION:-plasmawayland}"
export DISPLAY=""
export KDE_FULL_SESSION="${KDE_FULL_SESSION:-true}"
export KDE_SESSION_VERSION="${KDE_SESSION_VERSION:-6}"
export LIBSEAT_BACKEND="${LIBSEAT_BACKEND:-seatd}"
export LOGNAME="${LOGNAME:-${USER:-root}}"
export PATH="${PATH:-/usr/bin:/bin}"
export QML2_IMPORT_PATH="${QML2_IMPORT_PATH:-/usr/qml}"
export QT_PLUGIN_PATH="${QT_PLUGIN_PATH:-/usr/plugins}"
export QT_QPA_PLATFORM="${QT_QPA_PLATFORM:-wayland}"
export QT_QPA_PLATFORM_PLUGIN_PATH="${QT_QPA_PLATFORM_PLUGIN_PATH:-/usr/plugins/platforms}"
export SEATD_SOCK="${SEATD_SOCK:-/run/seatd.sock}"
export USER="${USER:-root}"
export WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}"
export XCURSOR_THEME="${XCURSOR_THEME:-Pop}"
export XDG_CURRENT_DESKTOP="${XDG_CURRENT_DESKTOP:-KDE}"
export XDG_SESSION_DESKTOP="${XDG_SESSION_DESKTOP:-KDE}"
export XDG_SESSION_ID="${XDG_SESSION_ID:-c1}"
export XDG_SESSION_TYPE="${XDG_SESSION_TYPE:-wayland}"
export XKB_CONFIG_ROOT="${XKB_CONFIG_ROOT:-/usr/share/X11/xkb}"
if [ -z "${XDG_RUNTIME_DIR:-}" ]; then
export XDG_RUNTIME_DIR="/tmp/run/user/$(id -u)"
fi
mkdir -p "$XDG_RUNTIME_DIR"
chmod 700 "$XDG_RUNTIME_DIR" 2>/dev/null || true
choose_state_dir() {
local requested="${REDBEAR_KDE_SESSION_STATE_DIR:-}"
if [ -n "$requested" ]; then
mkdir -p "$requested" 2>/dev/null || true
if [ -d "$requested" ] && [ -w "$requested" ]; then
printf '%s\n' "$requested"
return 0
fi
fi
if [ -d /run ] && [ -w /run ]; then
printf '%s\n' "/run"
return 0
fi
printf '%s\n' "$XDG_RUNTIME_DIR"
}
session_state_dir="$(choose_state_dir)"
mkdir -p "$session_state_dir"
chmod 700 "$session_state_dir" 2>/dev/null || true
session_env_file="$session_state_dir/redbear-kde-session.env"
session_ready_file="$session_state_dir/redbear-kde-session.ready"
panel_ready_file="$session_state_dir/redbear-kde-session.panel-ready"
rm -f "$session_ready_file" "$panel_ready_file"
cleanup() {
local status=$?
trap - EXIT INT TERM
for pid in "${optional_pids[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
fi
done
if [ -n "$kwin_pid" ] && kill -0 "$kwin_pid" 2>/dev/null; then
kill "$kwin_pid" 2>/dev/null || true
wait "$kwin_pid" 2>/dev/null || true
fi
exit "$status"
}
trap cleanup EXIT INT TERM
kwin_mode="virtual"
set_kwin_mode() {
local requested="${REDBEAR_KDE_SESSION_BACKEND:-auto}"
case "$requested" in
drm)
if [ -z "${KWIN_DRM_DEVICES:-}" ] && [ -e /scheme/drm/card0 ]; then
export KWIN_DRM_DEVICES=/scheme/drm/card0
fi
if [ -n "${KWIN_DRM_DEVICES:-}" ]; then
kwin_mode="drm"
else
kwin_mode="virtual"
fi
;;
virtual)
kwin_mode="virtual"
;;
auto|"")
if [ -n "${KWIN_DRM_DEVICES:-}" ]; then
kwin_mode="drm"
elif [ -e /scheme/drm/card0 ]; then
export KWIN_DRM_DEVICES=/scheme/drm/card0
kwin_mode="drm"
else
kwin_mode="virtual"
fi
;;
*)
kwin_mode="virtual"
;;
esac
}
set_kwin_mode
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && command -v dbus-launch >/dev/null 2>&1; then
eval "$(dbus-launch --sh-syntax)"
fi
write_session_environment() {
{
printf 'DBUS_SESSION_BUS_ADDRESS=%s\n' "${DBUS_SESSION_BUS_ADDRESS:-}"
printf 'DESKTOP_SESSION=%s\n' "$DESKTOP_SESSION"
printf 'KDE_FULL_SESSION=%s\n' "$KDE_FULL_SESSION"
printf 'KDE_SESSION_VERSION=%s\n' "$KDE_SESSION_VERSION"
printf 'KWIN_DRM_DEVICES=%s\n' "${KWIN_DRM_DEVICES:-}"
printf 'KWIN_MODE=%s\n' "$kwin_mode"
printf 'QML2_IMPORT_PATH=%s\n' "$QML2_IMPORT_PATH"
printf 'QT_PLUGIN_PATH=%s\n' "$QT_PLUGIN_PATH"
printf 'QT_QPA_PLATFORM=%s\n' "$QT_QPA_PLATFORM"
printf 'QT_QPA_PLATFORM_PLUGIN_PATH=%s\n' "$QT_QPA_PLATFORM_PLUGIN_PATH"
printf 'SEATD_SOCK=%s\n' "$SEATD_SOCK"
printf 'SESSION_STATE_DIR=%s\n' "$session_state_dir"
printf 'WAYLAND_DISPLAY=%s\n' "$WAYLAND_DISPLAY"
printf 'XDG_CURRENT_DESKTOP=%s\n' "$XDG_CURRENT_DESKTOP"
printf 'XDG_RUNTIME_DIR=%s\n' "$XDG_RUNTIME_DIR"
printf 'XDG_SESSION_DESKTOP=%s\n' "$XDG_SESSION_DESKTOP"
printf 'XDG_SESSION_ID=%s\n' "$XDG_SESSION_ID"
printf 'XDG_SESSION_TYPE=%s\n' "$XDG_SESSION_TYPE"
printf 'XKB_CONFIG_ROOT=%s\n' "$XKB_CONFIG_ROOT"
} > "$session_env_file"
chmod 600 "$session_env_file" 2>/dev/null || true
}
write_session_environment
if command -v dbus-update-activation-environment >/dev/null 2>&1; then
dbus-update-activation-environment \
DBUS_SESSION_BUS_ADDRESS \
DBUS_SESSION_BUS_PID \
DESKTOP_SESSION \
KDE_FULL_SESSION \
KDE_SESSION_VERSION \
KWIN_DRM_DEVICES \
QML2_IMPORT_PATH \
QT_PLUGIN_PATH \
QT_QPA_PLATFORM \
QT_QPA_PLATFORM_PLUGIN_PATH \
WAYLAND_DISPLAY \
XDG_CURRENT_DESKTOP \
XDG_RUNTIME_DIR \
XDG_SESSION_DESKTOP \
XDG_SESSION_ID \
XDG_SESSION_TYPE \
XKB_CONFIG_ROOT \
XCURSOR_THEME
fi
wait_for_wayland_socket() {
local socket_path="$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY"
local attempts=0
while [ "$attempts" -lt 40 ]; do
if [ -S "$socket_path" ] || [ -e "$socket_path" ]; then
return 0
fi
if [ -n "$kwin_pid" ] && ! kill -0 "$kwin_pid" 2>/dev/null; then
return 1
fi
attempts=$((attempts + 1))
sleep 1
done
return 1
}
mark_validation_success() {
if [ -e "$VALIDATION_REQUEST" ]; then
: > "$VALIDATION_SUCCESS" 2>/dev/null || true
fi
}
launch_optional_component() {
local program="$1"
local ready_marker="$2"
if ! command -v "$program" >/dev/null 2>&1; then
return 0
fi
"$program" &
local pid=$!
optional_pids+=("$pid")
if [ -n "$ready_marker" ]; then
sleep 1
if kill -0 "$pid" 2>/dev/null; then
: > "$ready_marker"
fi
fi
}
kwin_args=()
if [ "$kwin_mode" = "drm" ]; then
kwin_args+=(--drm)
else
kwin_args+=(--virtual)
fi
kwin_wayland_wrapper "${kwin_args[@]}" &
kwin_pid=$!
if ! wait_for_wayland_socket; then
printf '%s\n' "redbear-kde-session: kwin_wayland_wrapper failed to expose $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" >&2
exit 1
fi
: > "$session_ready_file"
mark_validation_success
launch_optional_component kded6 ""
launch_optional_component plasmashell "$panel_ready_file"
wait "$kwin_pid"
@@ -26,3 +26,4 @@ template = "cargo"
"/usr/bin/redbear-phase3-kwin-check" = "redbear-phase3-kwin-check"
"/usr/bin/redbear-phase4-kde-check" = "redbear-phase4-kde-check"
"/usr/bin/redbear-phase5-gpu-check" = "redbear-phase5-gpu-check"
"/usr/bin/redbear-phase5-cs-check" = "redbear-phase5-cs-check"
@@ -127,6 +127,10 @@ path = "src/bin/redbear-phase4-kde-check.rs"
name = "redbear-phase5-gpu-check"
path = "src/bin/redbear-phase5-gpu-check.rs"
[[bin]]
name = "redbear-phase5-cs-check"
path = "src/bin/redbear-phase5-cs-check.rs"
[dependencies]
redbear-login-protocol = { path = "../../redbear-login-protocol/source" }
serde = { version = "1", features = ["derive"] }
@@ -1,58 +1,129 @@
// Phase 4 KDE Plasma preflight check.
// Validates KF6 library presence, plasma binaries, and session entry points.
// Does NOT validate real KDE Plasma session behavior (blocked on Qt6Quick/QML + real KWin).
// Phase 4 KDE Plasma session check.
// Validates the installed KDE session entry point plus a bounded runtime surface
// exposed by the Red Bear session launcher and helper service.
use std::process;
const PROGRAM: &str = "redbear-phase4-kde-check";
const USAGE: &str = "Usage: redbear-phase4-kde-check [--json]\n\n\
Phase 4 KDE Plasma preflight check. Validates KF6 library and plasma binary\n\
presence. Does NOT validate real KDE session behavior (gated on Qt6Quick/QML).";
Phase 4 KDE Plasma session check. Validates KF6 library presence, the\n\
Red Bear KDE session entry point, KDE session environment capture, core\n\
helper processes, and a basic panel-readiness proxy.";
#[cfg(target_os = "redox")]
use std::{
collections::BTreeMap,
env, fs,
path::{Path, PathBuf},
process::Command,
};
#[cfg(target_os = "redox")]
const REDBEAR_KDE_SESSION_ENV_FILE: &str = "redbear-kde-session.env";
#[cfg(target_os = "redox")]
const REDBEAR_KDE_SESSION_READY_FILE: &str = "redbear-kde-session.ready";
#[cfg(target_os = "redox")]
const REDBEAR_KDE_SESSION_PANEL_READY_FILE: &str = "redbear-kde-session.panel-ready";
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult { Pass, Fail, Skip }
enum CheckResult {
Pass,
Fail,
Skip,
}
#[cfg(target_os = "redox")]
impl CheckResult {
fn label(self) -> &'static str {
match self { Self::Pass => "PASS", Self::Fail => "FAIL", Self::Skip => "SKIP" }
match self {
Self::Pass => "PASS",
Self::Fail => "FAIL",
Self::Skip => "SKIP",
}
}
}
#[cfg(target_os = "redox")]
struct Check { name: String, result: CheckResult, detail: String }
struct Check {
name: String,
result: CheckResult,
detail: String,
}
#[cfg(target_os = "redox")]
impl Check {
fn pass(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Pass, detail: detail.to_string() }
fn pass(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Pass,
detail: detail.into(),
}
}
fn fail(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Fail, detail: detail.to_string() }
fn fail(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Fail,
detail: detail.into(),
}
}
fn skip(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Skip, detail: detail.to_string() }
fn skip(name: &str, detail: impl Into<String>) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Skip,
detail: detail.into(),
}
}
}
#[cfg(target_os = "redox")]
struct Report { checks: Vec<Check>, json_mode: bool }
struct Report {
checks: Vec<Check>,
json_mode: bool,
}
#[cfg(target_os = "redox")]
impl Report {
fn new(json_mode: bool) -> Self { Report { checks: Vec::new(), json_mode } }
fn add(&mut self, check: Check) { self.checks.push(check); }
fn any_failed(&self) -> bool { self.checks.iter().any(|c| c.result == CheckResult::Fail) }
fn new(json_mode: bool) -> Self {
Self {
checks: Vec::new(),
json_mode,
}
}
fn add(&mut self, check: Check) {
self.checks.push(check);
}
fn any_failed(&self) -> bool {
self.checks
.iter()
.any(|check| check.result == CheckResult::Fail)
}
fn check_passed(&self, name: &str) -> bool {
self.checks
.iter()
.find(|check| check.name == name)
.is_some_and(|check| check.result == CheckResult::Pass)
}
fn print(&self) {
if self.json_mode { self.print_json(); } else { self.print_human(); }
if self.json_mode {
self.print_json();
} else {
self.print_human();
}
}
fn print_human(&self) {
for check in &self.checks {
let icon = match check.result {
CheckResult::Pass => "[PASS]", CheckResult::Fail => "[FAIL]", CheckResult::Skip => "[SKIP]",
CheckResult::Pass => "[PASS]",
CheckResult::Fail => "[FAIL]",
CheckResult::Skip => "[SKIP]",
};
println!("{icon} {}: {}", check.name, check.detail);
}
@@ -60,129 +131,573 @@ impl Report {
fn print_json(&self) {
#[derive(serde::Serialize)]
struct JsonCheck { name: String, result: String, detail: String }
struct JsonCheck {
name: String,
result: String,
detail: String,
}
#[derive(serde::Serialize)]
struct JsonReport {
kf6_libs_present: bool, plasma_binaries_present: bool,
session_entry: bool, kirigami_available: bool, checks: Vec<JsonCheck>,
overall_success: bool,
kf6_libs_present: bool,
plasma_binaries_present: bool,
session_entry: bool,
session_environment: bool,
plasmashell_process: bool,
kded6_process: bool,
panel_rendering_ready: bool,
kirigami_available: bool,
checks: Vec<JsonCheck>,
}
let kf6_libs = self.checks.iter().find(|c| c.name == "KF6_LIBRARIES").map_or(false, |c| c.result == CheckResult::Pass);
let plasma_bins = self.checks.iter().find(|c| c.name == "PLASMA_BINARIES").map_or(false, |c| c.result == CheckResult::Pass);
let session_entry = self.checks.iter().find(|c| c.name == "SESSION_ENTRY").map_or(false, |c| c.result == CheckResult::Pass);
let kirigami = self.checks.iter().find(|c| c.name == "KIRIGAMI_STATUS").map_or(false, |c| c.result == CheckResult::Pass);
let checks: Vec<JsonCheck> = self.checks.iter().map(|c| JsonCheck {
name: c.name.clone(), result: c.result.label().to_string(), detail: c.detail.clone(),
}).collect();
if let Err(err) = serde_json::to_writer(std::io::stdout(), &JsonReport { kf6_libs_present: kf6_libs, plasma_binaries_present: plasma_bins, session_entry, kirigami_available: kirigami, checks }) {
let checks = self
.checks
.iter()
.map(|check| JsonCheck {
name: check.name.clone(),
result: check.result.label().to_string(),
detail: check.detail.clone(),
})
.collect::<Vec<_>>();
let report = JsonReport {
overall_success: !self.any_failed(),
kf6_libs_present: self.check_passed("KF6_LIBRARIES"),
plasma_binaries_present: self.check_passed("PLASMA_BINARIES"),
session_entry: self.check_passed("SESSION_ENTRY"),
session_environment: self.check_passed("SESSION_ENVIRONMENT"),
plasmashell_process: self.check_passed("PLASMASHELL_PROCESS"),
kded6_process: self.check_passed("KDED6_PROCESS"),
panel_rendering_ready: self.check_passed("PANEL_RENDERING_READY"),
kirigami_available: self.check_passed("KIRIGAMI_STATUS"),
checks,
};
if let Err(err) = serde_json::to_writer(std::io::stdout(), &report) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
#[derive(Clone, Debug)]
struct SessionEnvironment {
source: String,
values: BTreeMap<String, String>,
}
#[cfg(target_os = "redox")]
fn parse_args() -> Result<bool, String> {
let mut json_mode = false;
for arg in std::env::args().skip(1) {
match arg.as_str() {
"--json" => json_mode = true,
"-h" | "--help" => { println!("{USAGE}"); return Err(String::new()); }
"-h" | "--help" => {
println!("{USAGE}");
return Err(String::new());
}
_ => return Err(format!("unsupported argument: {arg}")),
}
}
Ok(json_mode)
}
#[cfg(target_os = "redox")]
fn check_kf6_libraries() -> Check {
let key_libs = [
"/usr/lib/libKF6CoreAddons.so", "/usr/lib/libKF6ConfigCore.so",
"/usr/lib/libKF6I18n.so", "/usr/lib/libKF6WindowSystem.so",
"/usr/lib/libKF6Notifications.so", "/usr/lib/libKF6Service.so",
"/usr/lib/libKF6CoreAddons.so",
"/usr/lib/libKF6ConfigCore.so",
"/usr/lib/libKF6I18n.so",
"/usr/lib/libKF6WindowSystem.so",
"/usr/lib/libKF6Notifications.so",
"/usr/lib/libKF6Service.so",
"/usr/lib/libKF6WaylandClient.so",
];
let mut found = 0usize;
let mut missing = Vec::new();
for lib in key_libs {
if std::path::Path::new(lib).exists() {
if Path::new(lib).exists() {
found += 1;
} else {
missing.push(lib);
}
}
if found >= 6 {
let preview: Vec<_> = missing.iter().take(3).map(|s| s.rsplit('/').next().unwrap_or(s)).collect();
if missing.is_empty() {
Check::pass("KF6_LIBRARIES", &format!("{}/{} key KF6 libs found", found, key_libs.len()))
Check::pass(
"KF6_LIBRARIES",
format!("{found}/{} key KF6 libraries found", key_libs.len()),
)
} else {
Check::pass("KF6_LIBRARIES", &format!("{}/{} found, missing: {}", found, key_libs.len(), preview.join(", ")))
let preview = missing
.iter()
.take(3)
.map(|path| path.rsplit('/').next().unwrap_or(path))
.collect::<Vec<_>>()
.join(", ");
Check::pass(
"KF6_LIBRARIES",
format!("{found}/{} found, missing: {preview}", key_libs.len()),
)
}
} else {
Check::fail("KF6_LIBRARIES", &format!("only {}/{} key KF6 libs found", found, key_libs.len()))
Check::fail(
"KF6_LIBRARIES",
format!("only {found}/{} key KF6 libraries found", key_libs.len()),
)
}
}
#[cfg(target_os = "redox")]
fn check_plasma_binaries() -> Check {
let bins = ["/usr/bin/plasmashell", "/usr/bin/systemsettings", "/usr/bin/kwin_wayland_wrapper"];
let mut found = 0usize;
for bin in bins {
if std::path::Path::new(bin).exists() { found += 1; }
}
if found >= 2 {
Check::pass("PLASMA_BINARIES", &format!("{}/{} plasma binaries present", found, bins.len()))
} else if found == 1 {
Check::fail("PLASMA_BINARIES", &format!("only {}/{} plasma binaries present", found, bins.len()))
} else {
Check::fail("PLASMA_BINARIES", "no plasma binaries found")
let required = [
"/usr/bin/redbear-kde-session",
"/usr/bin/kwin_wayland_wrapper",
"/usr/bin/plasmashell",
"/usr/bin/kded6",
];
let optional: &[&str] = &[];
let missing_required = required
.iter()
.copied()
.filter(|path| !Path::new(path).exists())
.collect::<Vec<_>>();
if !missing_required.is_empty() {
return Check::fail(
"PLASMA_BINARIES",
format!(
"missing required session binaries: {}",
missing_required.join(", ")
),
);
}
let found_optional = optional
.iter()
.copied()
.filter(|path| Path::new(path).exists())
.collect::<Vec<_>>();
Check::pass(
"PLASMA_BINARIES",
format!(
"required session binaries present; optional helpers found: {}/{}",
found_optional.len(),
optional.len()
),
)
}
#[cfg(target_os = "redox")]
fn check_session_entry() -> Check {
let entries = ["/usr/bin/startplasma-wayland", "/usr/lib/plasma-session"];
for e in entries {
if std::path::Path::new(e).exists() {
return Check::pass("SESSION_ENTRY", e);
let entry = "/usr/bin/redbear-kde-session";
if Path::new(entry).exists() {
Check::pass("SESSION_ENTRY", entry)
} else {
Check::fail("SESSION_ENTRY", "missing /usr/bin/redbear-kde-session")
}
}
#[cfg(target_os = "redox")]
fn env_value(name: &str) -> Option<String> {
env::var(name).ok().filter(|value| !value.trim().is_empty())
}
#[cfg(target_os = "redox")]
fn candidate_state_dirs() -> Vec<PathBuf> {
let mut dirs = vec![
PathBuf::from("/run"),
PathBuf::from("/run/redbear-display-session"),
];
if let Some(dir) = env_value("XDG_RUNTIME_DIR") {
let runtime_dir = PathBuf::from(dir);
if !dirs.contains(&runtime_dir) {
dirs.push(runtime_dir);
}
}
Check::fail("SESSION_ENTRY", "no KDE session entry point found")
dirs
}
#[cfg(target_os = "redox")]
fn candidate_state_files(file_name: &str) -> Vec<PathBuf> {
candidate_state_dirs()
.into_iter()
.map(|dir| dir.join(file_name))
.collect::<Vec<_>>()
}
#[cfg(target_os = "redox")]
fn parse_key_value_file(path: &Path) -> Result<BTreeMap<String, String>, String> {
let contents = fs::read_to_string(path)
.map_err(|err| format!("failed to read {}: {err}", path.display()))?;
let mut values = BTreeMap::new();
for raw_line in contents.lines() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('=') {
values.insert(key.to_string(), value.to_string());
}
}
Ok(values)
}
#[cfg(target_os = "redox")]
fn load_session_environment() -> Result<SessionEnvironment, String> {
for path in candidate_state_files(REDBEAR_KDE_SESSION_ENV_FILE) {
if path.exists() {
let values = parse_key_value_file(&path)?;
return Ok(SessionEnvironment {
source: path.display().to_string(),
values,
});
}
}
let mut values = BTreeMap::new();
for key in [
"XDG_SESSION_TYPE",
"XDG_CURRENT_DESKTOP",
"KDE_FULL_SESSION",
"QT_PLUGIN_PATH",
"QT_QPA_PLATFORM_PLUGIN_PATH",
"QML2_IMPORT_PATH",
"WAYLAND_DISPLAY",
"XDG_RUNTIME_DIR",
] {
if let Some(value) = env_value(key) {
values.insert(key.to_string(), value);
}
}
if values.is_empty() {
let paths = candidate_state_files(REDBEAR_KDE_SESSION_ENV_FILE)
.into_iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ");
Err(format!("no KDE session environment file found in: {paths}"))
} else {
Ok(SessionEnvironment {
source: String::from("current process environment"),
values,
})
}
}
#[cfg(target_os = "redox")]
fn check_required_env_value(
values: &BTreeMap<String, String>,
key: &str,
expected: &str,
) -> Result<(), String> {
match values.get(key) {
Some(value) if value == expected => Ok(()),
Some(value) => Err(format!("{key}={value} (expected {expected})")),
None => Err(format!("missing {key}")),
}
}
#[cfg(target_os = "redox")]
fn check_nonempty_env_value(values: &BTreeMap<String, String>, key: &str) -> Result<(), String> {
match values.get(key) {
Some(value) if !value.trim().is_empty() => Ok(()),
Some(_) => Err(format!("{key} is empty")),
None => Err(format!("missing {key}")),
}
}
#[cfg(target_os = "redox")]
fn check_session_environment() -> Check {
match load_session_environment() {
Ok(session) => {
let checks = [
check_required_env_value(&session.values, "XDG_SESSION_TYPE", "wayland"),
check_required_env_value(&session.values, "XDG_CURRENT_DESKTOP", "KDE"),
check_required_env_value(&session.values, "KDE_FULL_SESSION", "true"),
check_nonempty_env_value(&session.values, "QT_PLUGIN_PATH"),
check_nonempty_env_value(&session.values, "QT_QPA_PLATFORM_PLUGIN_PATH"),
check_nonempty_env_value(&session.values, "QML2_IMPORT_PATH"),
];
let failures = checks
.into_iter()
.filter_map(Result::err)
.collect::<Vec<_>>();
if failures.is_empty() {
Check::pass(
"SESSION_ENVIRONMENT",
format!("captured KDE session environment from {}", session.source),
)
} else {
Check::fail(
"SESSION_ENVIRONMENT",
format!(
"invalid KDE session environment from {}: {}",
session.source,
failures.join("; ")
),
)
}
}
Err(err) => Check::fail("SESSION_ENVIRONMENT", err),
}
}
#[cfg(target_os = "redox")]
fn run_command(program: &str, args: &[&str], label: &str) -> Result<String, String> {
let output = Command::new(program)
.args(args)
.output()
.map_err(|err| format!("failed to run {label}: {err}"))?;
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let detail = if !stderr.trim().is_empty() {
stderr.trim().to_string()
} else if !stdout.trim().is_empty() {
stdout.trim().to_string()
} else {
String::from("no output")
};
return Err(format!(
"{label} exited with status {}: {detail}",
output.status
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
#[cfg(target_os = "redox")]
fn process_output() -> Result<String, String> {
run_command("ps", &[], "ps")
}
#[cfg(target_os = "redox")]
fn output_has_process(output: &str, process_name: &str) -> bool {
output.lines().any(|line| line.contains(process_name))
}
#[cfg(target_os = "redox")]
fn check_required_process(process_name: &str, binary_path: &str, check_name: &str) -> Check {
if !Path::new(binary_path).exists() {
return Check::fail(check_name, format!("{binary_path} is not installed"));
}
match process_output() {
Ok(output) => {
if output_has_process(&output, process_name) {
Check::pass(check_name, format!("{process_name} appears in ps output"))
} else {
Check::fail(
check_name,
format!("{process_name} is not present in ps output"),
)
}
}
Err(err) => Check::fail(check_name, err),
}
}
#[cfg(target_os = "redox")]
fn first_existing_state_file(file_name: &str) -> Option<PathBuf> {
candidate_state_files(file_name)
.into_iter()
.find(|path| path.exists())
}
#[cfg(target_os = "redox")]
fn wayland_socket_from_session_env(values: &BTreeMap<String, String>) -> Option<PathBuf> {
let runtime_dir = values.get("XDG_RUNTIME_DIR")?;
let display = values.get("WAYLAND_DISPLAY")?;
Some(PathBuf::from(runtime_dir).join(display))
}
#[cfg(target_os = "redox")]
fn check_panel_rendering_readiness() -> Check {
if !Path::new("/usr/bin/plasmashell").exists() {
return Check::skip(
"PANEL_RENDERING_READY",
"plasmashell is not installed, panel readiness cannot be checked",
);
}
if let Some(path) = first_existing_state_file(REDBEAR_KDE_SESSION_PANEL_READY_FILE) {
return Check::pass(
"PANEL_RENDERING_READY",
format!("panel readiness marker present at {}", path.display()),
);
}
let session = match load_session_environment() {
Ok(session) => session,
Err(err) => return Check::fail("PANEL_RENDERING_READY", err),
};
let socket_path = match wayland_socket_from_session_env(&session.values) {
Some(path) => path,
None => {
return Check::fail(
"PANEL_RENDERING_READY",
"session environment is missing XDG_RUNTIME_DIR or WAYLAND_DISPLAY",
);
}
};
let processes = match process_output() {
Ok(output) => output,
Err(err) => return Check::fail("PANEL_RENDERING_READY", err),
};
if output_has_process(&processes, "plasmashell") && socket_path.exists() {
Check::pass(
"PANEL_RENDERING_READY",
format!(
"plasmashell is running and Wayland socket is present at {}",
socket_path.display()
),
)
} else {
Check::fail(
"PANEL_RENDERING_READY",
format!(
"missing panel marker and runtime proxy (plasmashell process/socket {})",
socket_path.display()
),
)
}
}
#[cfg(target_os = "redox")]
fn check_session_ready_marker() -> Check {
if let Some(path) = first_existing_state_file(REDBEAR_KDE_SESSION_READY_FILE) {
Check::pass(
"SESSION_READY_MARKER",
format!("session readiness marker present at {}", path.display()),
)
} else {
let paths = candidate_state_files(REDBEAR_KDE_SESSION_READY_FILE)
.into_iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ");
Check::fail(
"SESSION_READY_MARKER",
format!("no readiness marker found in: {paths}"),
)
}
}
#[cfg(target_os = "redox")]
fn check_kirigami_status() -> Check {
let kirigami_lib = "/usr/lib/libKF6Kirigami.so";
if std::path::Path::new(kirigami_lib).exists() {
if Path::new(kirigami_lib).exists() {
Check::pass("KIRIGAMI_STATUS", "kirigami library present")
} else {
Check::skip("KIRIGAMI_STATUS", "kirigami not available (QML stub, requires Qt6Quick)")
Check::skip(
"KIRIGAMI_STATUS",
"kirigami not available (QML stub, requires Qt6Quick)",
)
}
}
fn run() -> Result<(), String> {
#[cfg(not(target_os = "redox"))]
{
if std::env::args().any(|a| a == "-h" || a == "--help") { println!("{USAGE}"); return Err(String::new()); }
if std::env::args().any(|arg| arg == "-h" || arg == "--help") {
println!("{USAGE}");
return Err(String::new());
}
println!("{PROGRAM}: KDE Plasma check requires Redox runtime");
return Ok(());
}
#[cfg(target_os = "redox")]
{
let json_mode = parse_args()?;
let mut report = Report::new(json_mode);
report.add(check_kf6_libraries());
report.add(check_plasma_binaries());
report.add(check_session_entry());
report.add(check_session_environment());
report.add(check_session_ready_marker());
report.add(check_required_process(
"plasmashell",
"/usr/bin/plasmashell",
"PLASMASHELL_PROCESS",
));
report.add(check_required_process(
"kded6",
"/usr/bin/kded6",
"KDED6_PROCESS",
));
report.add(check_panel_rendering_readiness());
report.add(check_kirigami_status());
report.print();
if report.any_failed() { return Err("one or more Phase 4 checks failed".to_string()); }
if report.any_failed() {
return Err(String::from("one or more Phase 4 KDE checks failed"));
}
Ok(())
}
}
fn main() {
if let Err(err) = run() {
if err.is_empty() { process::exit(0); }
if err.is_empty() {
process::exit(0);
}
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
#[cfg(all(test, target_os = "redox"))]
mod tests {
use super::*;
#[test]
fn parse_key_value_file_collects_session_values() {
let temp_dir = std::env::temp_dir().join("redbear-phase4-kde-check-tests");
fs::create_dir_all(&temp_dir).expect("temp dir should be created");
let path = temp_dir.join("env.txt");
fs::write(
&path,
"XDG_SESSION_TYPE=wayland\nKDE_FULL_SESSION=true\nQML2_IMPORT_PATH=/usr/qml\n",
)
.expect("env file should be written");
let parsed = parse_key_value_file(&path).expect("env file should parse");
assert_eq!(
parsed.get("XDG_SESSION_TYPE"),
Some(&String::from("wayland"))
);
assert_eq!(parsed.get("KDE_FULL_SESSION"), Some(&String::from("true")));
assert_eq!(
parsed.get("QML2_IMPORT_PATH"),
Some(&String::from("/usr/qml"))
);
}
#[test]
fn check_required_env_value_matches_expected_value() {
let mut values = BTreeMap::new();
values.insert(String::from("XDG_SESSION_TYPE"), String::from("wayland"));
assert!(check_required_env_value(&values, "XDG_SESSION_TYPE", "wayland").is_ok());
assert!(check_required_env_value(&values, "XDG_SESSION_TYPE", "x11").is_err());
}
}
@@ -0,0 +1,673 @@
// Phase 5 GPU command-submission validation checker.
// Validates DRM command-submission protocol reachability over /scheme/drm/card0.
// Does NOT claim real hardware render validation yet.
use std::process;
const PROGRAM: &str = "redbear-phase5-cs-check";
const USAGE: &str = "Usage: redbear-phase5-cs-check [--json]\n\n\
Phase 5 GPU command-submission validation. Probes DRM private CS ioctls,\n\
PRIME buffer sharing, GEM allocation, and fence/wait support. Real\n\
hardware rendering validation is still pending.";
#[cfg(target_os = "redox")]
const DRM_IOCTL_BASE: usize = 0x00A0;
#[cfg(target_os = "redox")]
const DRM_IOCTL_GEM_CREATE: usize = DRM_IOCTL_BASE + 26;
#[cfg(target_os = "redox")]
const DRM_IOCTL_GEM_CLOSE: usize = DRM_IOCTL_BASE + 27;
#[cfg(target_os = "redox")]
const DRM_IOCTL_PRIME_HANDLE_TO_FD: usize = DRM_IOCTL_BASE + 29;
#[cfg(target_os = "redox")]
const DRM_IOCTL_PRIME_FD_TO_HANDLE: usize = DRM_IOCTL_BASE + 30;
#[cfg(target_os = "redox")]
const DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT: usize = DRM_IOCTL_BASE + 31;
#[cfg(target_os = "redox")]
const DRM_IOCTL_REDOX_PRIVATE_CS_WAIT: usize = DRM_IOCTL_BASE + 32;
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult {
Pass,
Fail,
Skip,
}
#[cfg(target_os = "redox")]
impl CheckResult {
fn label(self) -> &'static str {
match self {
Self::Pass => "PASS",
Self::Fail => "FAIL",
Self::Skip => "SKIP",
}
}
}
#[cfg(target_os = "redox")]
struct Check {
name: String,
result: CheckResult,
detail: String,
}
#[cfg(target_os = "redox")]
impl Check {
fn pass(name: &str, detail: &str) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Pass,
detail: detail.to_string(),
}
}
fn fail(name: &str, detail: &str) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Fail,
detail: detail.to_string(),
}
}
fn skip(name: &str, detail: &str) -> Self {
Self {
name: name.to_string(),
result: CheckResult::Skip,
detail: detail.to_string(),
}
}
}
#[cfg(target_os = "redox")]
struct Report {
checks: Vec<Check>,
json_mode: bool,
}
#[cfg(target_os = "redox")]
impl Report {
fn new(json_mode: bool) -> Self {
Self {
checks: Vec::new(),
json_mode,
}
}
fn add(&mut self, check: Check) {
self.checks.push(check);
}
fn any_failed(&self) -> bool {
self.checks.iter().any(|check| check.result == CheckResult::Fail)
}
fn print(&self) {
if self.json_mode {
self.print_json();
} else {
self.print_human();
}
}
fn print_human(&self) {
for check in &self.checks {
let icon = match check.result {
CheckResult::Pass => "[PASS]",
CheckResult::Fail => "[FAIL]",
CheckResult::Skip => "[SKIP]",
};
println!("{icon} {}: {}", check.name, check.detail);
}
}
fn print_json(&self) {
#[derive(serde::Serialize)]
struct JsonCheck {
name: String,
result: String,
detail: String,
}
#[derive(serde::Serialize)]
struct JsonReport {
command_submission_protocol: bool,
prime_buffer_sharing: bool,
gem_buffer_allocation: bool,
fence_sync_support: bool,
hardware_validation_pending: bool,
checks: Vec<JsonCheck>,
}
let check_passed = |name: &str| {
self.checks
.iter()
.find(|check| check.name == name)
.is_some_and(|check| check.result == CheckResult::Pass)
};
let checks = self
.checks
.iter()
.map(|check| JsonCheck {
name: check.name.clone(),
result: check.result.label().to_string(),
detail: check.detail.clone(),
})
.collect::<Vec<_>>();
if let Err(err) = serde_json::to_writer(
std::io::stdout(),
&JsonReport {
command_submission_protocol: check_passed("CS_IOCTL_PROTOCOL"),
prime_buffer_sharing: check_passed("PRIME_BUFFER_SHARING"),
gem_buffer_allocation: check_passed("GEM_BUFFER_ALLOCATION"),
fence_sync_support: check_passed("FENCE_SYNC_SUPPORT"),
hardware_validation_pending: true,
checks,
},
) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmGemCreateWire {
size: u64,
handle: u32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmGemCloseWire {
handle: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmPrimeHandleToFdWire {
handle: u32,
flags: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmPrimeHandleToFdResponseWire {
fd: i32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmPrimeFdToHandleWire {
fd: i32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmPrimeFdToHandleResponseWire {
handle: u32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsSubmit {
src_handle: u32,
dst_handle: u32,
src_offset: u64,
dst_offset: u64,
byte_count: u64,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsSubmitResult {
seqno: u64,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsWait {
seqno: u64,
timeout_ns: u64,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsWaitResult {
completed: u8,
pad: [u8; 7],
completed_seqno: u64,
}
#[cfg(target_os = "redox")]
fn parse_args() -> Result<bool, String> {
let mut json_mode = false;
for arg in std::env::args().skip(1) {
match arg.as_str() {
"--json" => json_mode = true,
"-h" | "--help" => {
println!("{USAGE}");
return Err(String::new());
}
_ => return Err(format!("unsupported argument: {arg}")),
}
}
Ok(json_mode)
}
#[cfg(target_os = "redox")]
fn decode_wire_exact<T: Copy>(bytes: &[u8]) -> Result<T, String> {
use std::mem::{MaybeUninit, size_of};
if bytes.len() != size_of::<T>() {
return Err(format!(
"unexpected DRM response size: expected {} bytes, got {}",
size_of::<T>(),
bytes.len()
));
}
let mut out = MaybeUninit::<T>::uninit();
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_mut_ptr().cast::<u8>(), size_of::<T>());
Ok(out.assume_init())
}
}
#[cfg(target_os = "redox")]
fn bytes_of<T>(value: &T) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
(value as *const T).cast::<u8>(),
std::mem::size_of::<T>(),
)
}
}
#[cfg(target_os = "redox")]
fn open_drm_card(path: &str) -> Result<std::fs::File, String> {
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(|err| format!("failed to open {path}: {err}"))
}
#[cfg(target_os = "redox")]
fn drm_query(file: &mut std::fs::File, request: usize, payload: &[u8]) -> Result<Vec<u8>, String> {
use std::io::{Read, Write};
let mut request_buf = request.to_le_bytes().to_vec();
request_buf.extend_from_slice(payload);
file.write_all(&request_buf)
.map_err(|err| format!("failed to send DRM ioctl {request:#x}: {err}"))?;
let mut response = vec![0u8; 4096];
let len = file
.read(&mut response)
.map_err(|err| format!("failed to read DRM ioctl {request:#x} response: {err}"))?;
response.truncate(len);
Ok(response)
}
#[cfg(target_os = "redox")]
fn close_gem(file: &mut std::fs::File, handle: u32) {
let request = DrmGemCloseWire { handle };
let _ = drm_query(file, DRM_IOCTL_GEM_CLOSE, bytes_of(&request));
}
#[cfg(target_os = "redox")]
fn run_redox(json_mode: bool) -> Result<(), String> {
let mut report = Report::new(json_mode);
let card_path = "/scheme/drm/card0";
if !std::path::Path::new(card_path).exists() {
report.add(Check::fail(
"CS_IOCTL_PROTOCOL",
"/scheme/drm/card0 missing; cannot probe command submission",
));
report.add(Check::skip(
"GEM_BUFFER_ALLOCATION",
"blocked: DRM card is unavailable",
));
report.add(Check::skip(
"PRIME_BUFFER_SHARING",
"blocked: DRM card is unavailable",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: DRM card is unavailable",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
let mut exporter = match open_drm_card(card_path) {
Ok(file) => file,
Err(err) => {
report.add(Check::fail("CS_IOCTL_PROTOCOL", &err));
report.add(Check::skip(
"GEM_BUFFER_ALLOCATION",
"blocked: DRM card could not be opened",
));
report.add(Check::skip(
"PRIME_BUFFER_SHARING",
"blocked: DRM card could not be opened",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: DRM card could not be opened",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
};
let mut importer = match open_drm_card(card_path) {
Ok(file) => file,
Err(err) => {
report.add(Check::fail("CS_IOCTL_PROTOCOL", &format!("opened exporter but importer failed: {err}")));
report.add(Check::skip(
"GEM_BUFFER_ALLOCATION",
"blocked: second DRM handle could not be opened",
));
report.add(Check::skip(
"PRIME_BUFFER_SHARING",
"blocked: second DRM handle could not be opened",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: second DRM handle could not be opened",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
};
let mut exporter_handle = None;
let mut importer_src_handle = None;
let mut importer_dst_handle = None;
let create_exporter = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
match drm_query(&mut exporter, DRM_IOCTL_GEM_CREATE, bytes_of(&create_exporter))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => {
exporter_handle = Some(created.handle);
report.add(Check::pass(
"GEM_BUFFER_ALLOCATION",
&format!("allocated exporter GEM handle {} (4096 bytes)", created.handle),
));
}
Err(err) => {
report.add(Check::fail("GEM_BUFFER_ALLOCATION", &err));
report.add(Check::skip(
"PRIME_BUFFER_SHARING",
"blocked: GEM allocation failed",
));
report.add(Check::skip(
"CS_IOCTL_PROTOCOL",
"blocked: GEM allocation failed",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: GEM allocation failed",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
}
if let Some(handle) = exporter_handle {
let export = DrmPrimeHandleToFdWire { handle, flags: 0 };
let prime_result = drm_query(&mut exporter, DRM_IOCTL_PRIME_HANDLE_TO_FD, bytes_of(&export))
.and_then(|response| decode_wire_exact::<DrmPrimeHandleToFdResponseWire>(&response))
.and_then(|exported| {
if exported.fd < 0 {
return Err(format!(
"PRIME export returned invalid token {} for GEM {}",
exported.fd, handle
));
}
let import = DrmPrimeFdToHandleWire {
fd: exported.fd,
pad: 0,
};
drm_query(&mut importer, DRM_IOCTL_PRIME_FD_TO_HANDLE, bytes_of(&import))
.and_then(|response| decode_wire_exact::<DrmPrimeFdToHandleResponseWire>(&response))
.map(|imported| (exported.fd, imported.handle))
});
match prime_result {
Ok((token, imported_handle)) => {
importer_src_handle = Some(imported_handle);
report.add(Check::pass(
"PRIME_BUFFER_SHARING",
&format!(
"export token {} imported as GEM handle {} on a second DRM fd",
token, imported_handle
),
));
}
Err(err) => {
report.add(Check::fail("PRIME_BUFFER_SHARING", &err));
report.add(Check::skip(
"CS_IOCTL_PROTOCOL",
"blocked: PRIME import/export failed",
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: PRIME import/export failed",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
close_gem(&mut exporter, handle);
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
}
}
let create_importer = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
match drm_query(&mut importer, DRM_IOCTL_GEM_CREATE, bytes_of(&create_importer))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => importer_dst_handle = Some(created.handle),
Err(err) => {
report.add(Check::fail(
"CS_IOCTL_PROTOCOL",
&format!("secondary GEM allocation for CS submit failed: {err}"),
));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: no destination GEM for CS submit",
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"real hardware rendering validation still requires bare-metal evidence",
));
if let Some(handle) = importer_src_handle {
close_gem(&mut importer, handle);
}
if let Some(handle) = exporter_handle {
close_gem(&mut exporter, handle);
}
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
}
let submit_result = match (importer_src_handle, importer_dst_handle) {
(Some(src_handle), Some(dst_handle)) => {
let submit = RedoxPrivateCsSubmit {
src_handle,
dst_handle,
src_offset: 0,
dst_offset: 0,
byte_count: 64,
};
drm_query(
&mut importer,
DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT,
bytes_of(&submit),
)
.and_then(|response| decode_wire_exact::<RedoxPrivateCsSubmitResult>(&response))
.map(|result| (src_handle, dst_handle, result.seqno))
}
_ => Err("command submission prerequisites were incomplete".to_string()),
};
match submit_result {
Ok((src_handle, dst_handle, seqno)) => {
report.add(Check::pass(
"CS_IOCTL_PROTOCOL",
&format!(
"private CS submit accepted shared GEM {} -> local GEM {} (seqno {})",
src_handle, dst_handle, seqno
),
));
let wait = RedoxPrivateCsWait {
seqno,
timeout_ns: 0,
};
match drm_query(
&mut importer,
DRM_IOCTL_REDOX_PRIVATE_CS_WAIT,
bytes_of(&wait),
)
.and_then(|response| decode_wire_exact::<RedoxPrivateCsWaitResult>(&response))
{
Ok(wait_result) => {
let completed = match wait_result.completed {
0 => false,
1 => true,
value => {
report.add(Check::fail(
"FENCE_SYNC_SUPPORT",
&format!(
"wait ioctl returned invalid completion flag {} for seqno {}",
value, seqno
),
));
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"protocol-level CS proof exists, but real hardware rendering validation is still pending",
));
report.print();
return Err("one or more Phase 5 CS checks failed".to_string());
}
};
report.add(Check::pass(
"FENCE_SYNC_SUPPORT",
&format!(
"bounded wait ioctl responded for seqno {} (completed={}, completed_seqno={}); real sync-object validation is still pending",
seqno, completed, wait_result.completed_seqno
),
));
}
Err(err) => {
report.add(Check::fail("FENCE_SYNC_SUPPORT", &err));
}
}
}
Err(err) => {
report.add(Check::fail("CS_IOCTL_PROTOCOL", &err));
report.add(Check::skip(
"FENCE_SYNC_SUPPORT",
"blocked: command submission ioctl failed",
));
}
}
if let Some(handle) = importer_dst_handle {
close_gem(&mut importer, handle);
}
if let Some(handle) = importer_src_handle {
close_gem(&mut importer, handle);
}
if let Some(handle) = exporter_handle {
close_gem(&mut exporter, handle);
}
report.add(Check::skip(
"HARDWARE_VALIDATION_PENDING",
"protocol-level CS proof exists, but real hardware rendering validation is still pending",
));
report.print();
if report.any_failed() {
return Err("one or more Phase 5 CS checks failed".to_string());
}
Ok(())
}
fn run() -> Result<(), String> {
#[cfg(not(target_os = "redox"))]
{
if std::env::args().any(|arg| arg == "-h" || arg == "--help") {
println!("{USAGE}");
return Err(String::new());
}
println!("{PROGRAM}: CS check requires Redox runtime");
Ok(())
}
#[cfg(target_os = "redox")]
{
let json_mode = parse_args()?;
run_redox(json_mode)
}
}
fn main() {
if let Err(err) = run() {
if err.is_empty() {
process::exit(0);
}
eprintln!("{PROGRAM}: {err}");
process::exit(1);
}
}
@@ -10,6 +10,15 @@ const USAGE: &str = "Usage: redbear-phase5-gpu-check [--json]\n\n\
GPU firmware, and Mesa rendering infrastructure. Hardware validation\n\
requires real AMD/Intel GPU + command submission (CS ioctl).";
#[cfg(target_os = "redox")]
const DRM_IOCTL_BASE: usize = 0x00A0;
#[cfg(target_os = "redox")]
const DRM_IOCTL_GEM_CREATE: usize = DRM_IOCTL_BASE + 26;
#[cfg(target_os = "redox")]
const DRM_IOCTL_GEM_CLOSE: usize = DRM_IOCTL_BASE + 27;
#[cfg(target_os = "redox")]
const DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT: usize = DRM_IOCTL_BASE + 31;
#[cfg(target_os = "redox")]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CheckResult { Pass, Fail, Skip }
@@ -65,21 +74,68 @@ impl Report {
#[derive(serde::Serialize)]
struct JsonReport {
drm_device: bool, gpu_firmware: bool, mesa_dri: bool,
display_modes: bool, checks: Vec<JsonCheck>,
display_modes: bool, cs_ioctl: bool, gem_buffers: bool,
hardware_rendering_ready: bool, checks: Vec<JsonCheck>,
}
let drm = self.checks.iter().find(|c| c.name == "DRM_DEVICE").map_or(false, |c| c.result == CheckResult::Pass);
let firmware = self.checks.iter().find(|c| c.name == "GPU_FIRMWARE").map_or(false, |c| c.result == CheckResult::Pass);
let mesa = self.checks.iter().find(|c| c.name == "MESA_DRI").map_or(false, |c| c.result == CheckResult::Pass);
let modes = self.checks.iter().find(|c| c.name == "DISPLAY_MODES").map_or(false, |c| c.result == CheckResult::Pass);
let cs_ioctl = self.checks.iter().find(|c| c.name == "CS_IOCTL_PROTOCOL").map_or(false, |c| c.result == CheckResult::Pass);
let gem_buffers = self.checks.iter().find(|c| c.name == "GEM_BUFFER_ALLOCATION").map_or(false, |c| c.result == CheckResult::Pass);
let hardware_ready = self.checks.iter().find(|c| c.name == "HARDWARE_RENDERING_READY").map_or(false, |c| c.result == CheckResult::Pass);
let checks: Vec<JsonCheck> = self.checks.iter().map(|c| JsonCheck {
name: c.name.clone(), result: c.result.label().to_string(), detail: c.detail.clone(),
}).collect();
if let Err(err) = serde_json::to_writer(std::io::stdout(), &JsonReport { drm_device: drm, gpu_firmware: firmware, mesa_dri: mesa, display_modes: modes, checks }) {
if let Err(err) = serde_json::to_writer(std::io::stdout(), &JsonReport {
drm_device: drm,
gpu_firmware: firmware,
mesa_dri: mesa,
display_modes: modes,
cs_ioctl,
gem_buffers,
hardware_rendering_ready: hardware_ready,
checks,
}) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmGemCreateWire {
size: u64,
handle: u32,
pad: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct DrmGemCloseWire {
handle: u32,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsSubmit {
src_handle: u32,
dst_handle: u32,
src_offset: u64,
dst_offset: u64,
byte_count: u64,
}
#[cfg(target_os = "redox")]
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct RedoxPrivateCsSubmitResult {
seqno: u64,
}
#[cfg(target_os = "redox")]
fn parse_args() -> Result<bool, String> {
let mut json_mode = false;
@@ -95,13 +151,18 @@ fn parse_args() -> Result<bool, String> {
#[cfg(target_os = "redox")]
fn check_drm_device() -> Check {
let paths = ["/scheme/drm/card0", "/dev/dri/card0"];
for p in paths {
if std::path::Path::new(p).exists() {
return Check::pass("DRM_DEVICE", p);
}
let scheme_path = "/scheme/drm/card0";
if std::path::Path::new(scheme_path).exists() {
return Check::pass("DRM_DEVICE", scheme_path);
}
Check::fail("DRM_DEVICE", "no DRM device found at /scheme/drm/card0 or /dev/dri/card0")
let dev_alias = "/dev/dri/card0";
if std::path::Path::new(dev_alias).exists() {
return Check::fail(
"DRM_DEVICE",
"/dev/dri/card0 exists, but Phase 5 CS probing requires /scheme/drm/card0",
);
}
Check::fail("DRM_DEVICE", "no DRM device found at /scheme/drm/card0")
}
#[cfg(target_os = "redox")]
@@ -155,6 +216,216 @@ fn check_display_modes() -> Check {
}
}
#[cfg(target_os = "redox")]
fn decode_wire_exact<T: Copy>(bytes: &[u8]) -> Result<T, String> {
use std::mem::{MaybeUninit, size_of};
if bytes.len() != size_of::<T>() {
return Err(format!(
"unexpected DRM response size: expected {} bytes, got {}",
size_of::<T>(),
bytes.len()
));
}
let mut out = MaybeUninit::<T>::uninit();
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_mut_ptr().cast::<u8>(), size_of::<T>());
Ok(out.assume_init())
}
}
#[cfg(target_os = "redox")]
fn bytes_of<T>(value: &T) -> &[u8] {
unsafe {
std::slice::from_raw_parts(
(value as *const T).cast::<u8>(),
std::mem::size_of::<T>(),
)
}
}
#[cfg(target_os = "redox")]
fn open_scheme_drm_card() -> Result<std::fs::File, String> {
std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/scheme/drm/card0")
.map_err(|err| format!("failed to open /scheme/drm/card0: {err}"))
}
#[cfg(target_os = "redox")]
fn drm_query(file: &mut std::fs::File, request: usize, payload: &[u8]) -> Result<Vec<u8>, String> {
use std::io::{Read, Write};
let mut request_buf = request.to_le_bytes().to_vec();
request_buf.extend_from_slice(payload);
file.write_all(&request_buf)
.map_err(|err| format!("failed to send DRM ioctl {request:#x}: {err}"))?;
let mut response = vec![0u8; 4096];
let len = file
.read(&mut response)
.map_err(|err| format!("failed to read DRM ioctl {request:#x} response: {err}"))?;
response.truncate(len);
Ok(response)
}
#[cfg(target_os = "redox")]
fn check_gem_buffer_allocation() -> Check {
let mut card = match open_scheme_drm_card() {
Ok(card) => card,
Err(err) => return Check::fail("GEM_BUFFER_ALLOCATION", &err),
};
let request = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
match drm_query(&mut card, DRM_IOCTL_GEM_CREATE, bytes_of(&request))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => {
let _ = drm_query(
&mut card,
DRM_IOCTL_GEM_CLOSE,
bytes_of(&DrmGemCloseWire {
handle: created.handle,
}),
);
Check::pass(
"GEM_BUFFER_ALLOCATION",
&format!("allocated GEM handle {} over /scheme/drm/card0", created.handle),
)
}
Err(err) => Check::fail("GEM_BUFFER_ALLOCATION", &err),
}
}
#[cfg(target_os = "redox")]
fn check_cs_ioctl_protocol() -> Check {
let mut card = match open_scheme_drm_card() {
Ok(card) => card,
Err(err) => return Check::fail("CS_IOCTL_PROTOCOL", &err),
};
let first = DrmGemCreateWire {
size: 4096,
..DrmGemCreateWire::default()
};
let second = first;
let created_a = match drm_query(&mut card, DRM_IOCTL_GEM_CREATE, bytes_of(&first))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => created,
Err(err) => {
return Check::fail(
"CS_IOCTL_PROTOCOL",
&format!("source GEM allocation failed before CS probe: {err}"),
);
}
};
let created_b = match drm_query(&mut card, DRM_IOCTL_GEM_CREATE, bytes_of(&second))
.and_then(|response| decode_wire_exact::<DrmGemCreateWire>(&response))
{
Ok(created) => created,
Err(err) => {
let _ = drm_query(
&mut card,
DRM_IOCTL_GEM_CLOSE,
bytes_of(&DrmGemCloseWire {
handle: created_a.handle,
}),
);
return Check::fail(
"CS_IOCTL_PROTOCOL",
&format!("destination GEM allocation failed before CS probe: {err}"),
);
}
};
let submit = RedoxPrivateCsSubmit {
src_handle: created_a.handle,
dst_handle: created_b.handle,
src_offset: 0,
dst_offset: 0,
byte_count: 64,
};
let result = drm_query(
&mut card,
DRM_IOCTL_REDOX_PRIVATE_CS_SUBMIT,
bytes_of(&submit),
)
.and_then(|response| decode_wire_exact::<RedoxPrivateCsSubmitResult>(&response));
let _ = drm_query(
&mut card,
DRM_IOCTL_GEM_CLOSE,
bytes_of(&DrmGemCloseWire {
handle: created_b.handle,
}),
);
let _ = drm_query(
&mut card,
DRM_IOCTL_GEM_CLOSE,
bytes_of(&DrmGemCloseWire {
handle: created_a.handle,
}),
);
match result {
Ok(response) => Check::pass(
"CS_IOCTL_PROTOCOL",
&format!(
"private CS submit accepted GEM {} -> {} (seqno {})",
created_a.handle, created_b.handle, response.seqno
),
),
Err(err) => Check::fail("CS_IOCTL_PROTOCOL", &err),
}
}
#[cfg(target_os = "redox")]
fn check_hardware_rendering_ready(report: &Report) -> Check {
let required = [
"DRM_DEVICE",
"GPU_FIRMWARE",
"MESA_DRI",
"DISPLAY_MODES",
"GEM_BUFFER_ALLOCATION",
"CS_IOCTL_PROTOCOL",
];
let missing = required
.iter()
.copied()
.filter(|name| {
!report
.checks
.iter()
.any(|check| check.name == *name && check.result == CheckResult::Pass)
})
.collect::<Vec<_>>();
if missing.is_empty() {
Check::pass(
"HARDWARE_RENDERING_READY",
"Phase 5 preflight prerequisites are present; real hardware rendering validation is still pending",
)
} else {
Check::fail(
"HARDWARE_RENDERING_READY",
&format!(
"missing hardware rendering prerequisites: {}",
missing.join(", ")
),
)
}
}
fn run() -> Result<(), String> {
#[cfg(not(target_os = "redox"))]
{
@@ -170,6 +441,10 @@ fn run() -> Result<(), String> {
report.add(check_gpu_firmware());
report.add(check_mesa_dri_hardware());
report.add(check_display_modes());
report.add(check_gem_buffer_allocation());
report.add(check_cs_ioctl_protocol());
let readiness = check_hardware_rendering_ready(&report);
report.add(readiness);
report.print();
if report.any_failed() { return Err("one or more Phase 5 checks failed".to_string()); }
Ok(())