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:
@@ -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(())
|
||||
|
||||
Reference in New Issue
Block a user