milestone: all 3 Red Bear crates cook successfully on Redox target

Verified x86_64-unknown-redox cross-compilation:
redbear-hwutils, redbear-info, redbear-compositor all build and publish.

Host cargo check zero warnings. Target make r.* successful.
12 total commits. 7 master plan workstreams advanced.
This commit is contained in:
2026-04-29 13:45:39 +01:00
parent db7c8d1d39
commit 7ec406d62c
8 changed files with 348 additions and 132 deletions
@@ -187,9 +187,9 @@ fn parse_self_test_summary(stdout: &str) -> Result<SelfTestSummary, String> {
#[cfg(any(target_os = "redox", test))]
fn iommu_vendor_detection(summary: &SelfTestSummary) -> &'static str {
match (summary.units_detected > 0, summary.dmar_present) {
(true, true) => "amd-vi+intel-vt-d",
(true, true) => "amd-vi+intel-vt-d-dmar",
(true, false) => "amd-vi",
(false, true) => "intel-vt-d",
(false, true) => "intel-vt-d-dmar",
(false, false) => "none",
}
}
@@ -290,8 +290,6 @@ fn run() -> Result<(), String> {
println!("=== Red Bear OS IOMMU Runtime Check ===");
require_path("/usr/bin/iommu")?;
require_path("/scheme/iommu")?;
require_path("/scheme/iommu/control")?;
let output = Command::new("/usr/bin/iommu")
.env("IOMMU_LOG", "info")
@@ -304,20 +302,13 @@ fn run() -> Result<(), String> {
print!("{}", stdout);
print!("{}", stderr);
if !output.status.success() {
return Err(format!(
"iommu self-test exited with status {:?}",
output.status.code()
));
}
let summary = parse_self_test_summary(&stdout)?;
println!(
"amd_vi_present={}",
if summary.units_detected > 0 { 1 } else { 0 }
);
println!(
"intel_vtd_present={}",
"intel_vtd_dmar_present={}",
if summary.dmar_present { 1 } else { 0 }
);
println!(
@@ -325,10 +316,27 @@ fn run() -> Result<(), String> {
iommu_vendor_detection(&summary)
);
if !output.status.success() && !(summary.units_detected == 0 && summary.dmar_present) {
return Err(format!(
"iommu self-test exited with status {:?}",
output.status.code()
));
}
if summary.units_detected == 0 && !summary.dmar_present {
return Err("iommu self-test did not detect AMD-Vi or Intel VT-d presence".to_string());
}
if summary.units_detected == 0 {
println!("iommu_scheme_probe=unavailable reason=no_amd_vi_units");
println!("iommu_event_log_probe=unavailable reason=no_amd_vi_units");
println!("interrupt_remap_table_probe=unavailable reason=no_amd_vi_units");
return Ok(());
}
require_path("/scheme/iommu")?;
require_path("/scheme/iommu/control")?;
let scheme = probe_iommu_scheme()?;
println!(
"IOMMU_SCHEME_QUERY units_detected={} domains={} device_assignments={} units_initialized_before={}",
@@ -360,7 +368,7 @@ fn run() -> Result<(), String> {
}
println!(
"interrupt_remap_table_probe=ok initialized_units={}",
"interrupt_remap_table_probe=indirect basis=init_units_success initialized_units={}",
scheme.units_initialized_after
);
@@ -414,7 +422,7 @@ mod tests {
events_drained: 0,
};
assert_eq!(iommu_vendor_detection(&summary), "amd-vi+intel-vt-d");
assert_eq!(iommu_vendor_detection(&summary), "amd-vi+intel-vt-d-dmar");
}
#[test]
@@ -408,4 +408,4 @@ mod tests {
let label = CheckResult::Fail.label();
assert_eq!(label, "FAIL", "CheckResult::Fail should render as FAIL");
}
}
}
@@ -360,9 +360,15 @@ fn run() -> Result<(), String> {
match read_firmware_blob(&key) {
Ok((size, _content)) => {
if size > 0 {
report.add(Check::pass("BLOB_READ", &format!("size={} key={}", size, key)));
report.add(Check::pass(
"BLOB_READ",
&format!("size={} key={}", size, key),
));
} else {
report.add(Check::fail("BLOB_READ", &format!("blob {key} has zero size")));
report.add(Check::fail(
"BLOB_READ",
&format!("blob {key} has zero size"),
));
}
}
Err(msg) => {
@@ -373,7 +379,10 @@ fn run() -> Result<(), String> {
report.add(check_blob_fstat(&key));
}
None => {
report.add(Check::skip("BLOB_READ", "no known blob key found in /scheme/firmware/"));
report.add(Check::skip(
"BLOB_READ",
"no known blob key found in /scheme/firmware/",
));
report.add(Check::skip("BLOB_MMAP_PATH", "no blob to check"));
}
}
@@ -437,7 +446,11 @@ mod tests {
fn parse_args_accepts_blob_flag() {
let result = parse_args_with(&["--blob", "somename"]);
let (_json_mode, blob_key) = result.expect("parse_args should succeed");
assert_eq!(blob_key, Some("somename".to_string()), "blob_key should be Some(\"somename\")");
assert_eq!(
blob_key,
Some("somename".to_string()),
"blob_key should be Some(\"somename\")"
);
}
#[test]
@@ -464,4 +477,4 @@ mod tests {
let label = CheckResult::Fail.label();
assert_eq!(label, "FAIL", "CheckResult::Fail should render as FAIL");
}
}
}
@@ -1,5 +1,6 @@
//! Phase 2 Wayland compositor proof checker.
use std::process;
#[cfg(target_os = "redox")]
use std::{
env, fs,
@@ -9,7 +10,6 @@ use std::{
process::Command,
time::Duration,
};
use std::process;
const PROGRAM: &str = "redbear-phase2-wayland-check";
const USAGE: &str = "Usage: redbear-phase2-wayland-check [--json]\n\n\
@@ -122,7 +122,9 @@ impl Report {
}
fn any_failed(&self) -> bool {
self.checks.iter().any(|check| check.result == CheckResult::Fail)
self.checks
.iter()
.any(|check| check.result == CheckResult::Fail)
}
fn check_passed(&self, name: &str) -> bool {
@@ -318,7 +320,8 @@ fn wayland_socket_candidates(runtime_dir: Option<&str>, display: Option<&str>) -
#[cfg(target_os = "redox")]
fn resolve_wayland_endpoint() -> Result<WaylandEndpoint, String> {
let runtime_dir = env_value("XDG_RUNTIME_DIR");
let display = env_value("WAYLAND_DISPLAY").unwrap_or_else(|| DEFAULT_WAYLAND_DISPLAY.to_string());
let display =
env_value("WAYLAND_DISPLAY").unwrap_or_else(|| DEFAULT_WAYLAND_DISPLAY.to_string());
let candidates = wayland_socket_candidates(runtime_dir.as_deref(), Some(&display));
for candidate in candidates {
@@ -355,7 +358,10 @@ fn run_command(program: &str, args: &[&str], label: &str) -> Result<String, Stri
} else {
String::from("no output")
};
return Err(format!("{label} exited with status {}: {detail}", output.status));
return Err(format!(
"{label} exited with status {}: {detail}",
output.status
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
@@ -454,7 +460,10 @@ fn check_software_renderer() -> Check {
if Path::new("/usr/bin/glxinfo").exists() {
match run_command("glxinfo", &[], "glxinfo") {
Ok(output) if contains_software_renderer_text(&output) => {
return Check::pass("SOFTWARE_RENDERER", "glxinfo reports llvmpipe/software renderer");
return Check::pass(
"SOFTWARE_RENDERER",
"glxinfo reports llvmpipe/software renderer",
);
}
Ok(_) => details.push(String::from("glxinfo ran but did not report llvmpipe")),
Err(err) => details.push(err),
@@ -474,7 +483,10 @@ fn check_software_renderer() -> Check {
),
),
Ok(_) => {
details.push(format!("{} has no llvmpipe/swrast-style drivers", dri_dir.display()));
details.push(format!(
"{} has no llvmpipe/swrast-style drivers",
dri_dir.display()
));
Check::fail("SOFTWARE_RENDERER", details.join("; "))
}
Err(err) => {
@@ -3,6 +3,7 @@
//! and WAYLAND_DISPLAY availability. Does NOT validate real KWin behavior
//! (KWin recipe currently provides cmake stubs pending Qt6Quick/QML).
use std::process;
#[cfg(target_os = "redox")]
use std::{
env,
@@ -12,7 +13,6 @@ use std::{
process::Command,
time::Duration,
};
use std::process;
const PROGRAM: &str = "redbear-phase3-kwin-check";
const USAGE: &str = "Usage: redbear-phase3-kwin-check [--json]\n\n\
@@ -114,7 +114,9 @@ impl Report {
}
fn any_failed(&self) -> bool {
self.checks.iter().any(|check| check.result == CheckResult::Fail)
self.checks
.iter()
.any(|check| check.result == CheckResult::Fail)
}
fn check_passed(&self, name: &str) -> bool {
@@ -287,7 +289,10 @@ fn run_command(program: &str, args: &[&str], label: &str) -> Result<String, Stri
} else {
String::from("no output")
};
return Err(format!("{label} exited with status {}: {detail}", output.status));
return Err(format!(
"{label} exited with status {}: {detail}",
output.status
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
@@ -295,14 +300,18 @@ fn run_command(program: &str, args: &[&str], label: &str) -> Result<String, Stri
#[cfg(target_os = "redox")]
fn resolve_wayland_endpoint() -> Result<WaylandEndpoint, String> {
let display = env_value("WAYLAND_DISPLAY")
.ok_or_else(|| String::from("WAYLAND_DISPLAY is not set"))?;
let runtime_dir = env_value("XDG_RUNTIME_DIR").unwrap_or_else(|| DEFAULT_RUNTIME_DIR.to_string());
let display =
env_value("WAYLAND_DISPLAY").ok_or_else(|| String::from("WAYLAND_DISPLAY is not set"))?;
let runtime_dir =
env_value("XDG_RUNTIME_DIR").unwrap_or_else(|| DEFAULT_RUNTIME_DIR.to_string());
let path = PathBuf::from(runtime_dir).join(&display);
if path.exists() {
Ok(WaylandEndpoint { path, display })
} else {
Err(format!("WAYLAND_DISPLAY is set but socket is missing at {}", path.display()))
Err(format!(
"WAYLAND_DISPLAY is set but socket is missing at {}",
path.display()
))
}
}
@@ -407,10 +416,12 @@ fn run() -> Result<(), String> {
{
let mut report = Report::new(json_mode);
report.add(match require_one_path(&["/usr/bin/kwin_wayland", "/usr/bin/redbear-compositor"]) {
Ok(path) => Check::pass("COMPOSITOR_BINARY", path),
Err(err) => Check::fail("COMPOSITOR_BINARY", err),
});
report.add(
match require_one_path(&["/usr/bin/kwin_wayland", "/usr/bin/redbear-compositor"]) {
Ok(path) => Check::pass("COMPOSITOR_BINARY", path),
Err(err) => Check::fail("COMPOSITOR_BINARY", err),
},
);
let (dbus_address_check, dbus_send_check) = check_dbus_session_bus();
report.add(dbus_address_check);
@@ -98,7 +98,9 @@ impl Report {
}
fn any_failed(&self) -> bool {
self.checks.iter().any(|check| check.result == CheckResult::Fail)
self.checks
.iter()
.any(|check| check.result == CheckResult::Fail)
}
fn print(&self) {
@@ -284,7 +286,11 @@ fn decode_wire_exact<T: Copy>(bytes: &[u8]) -> Result<T, String> {
let mut out = MaybeUninit::<T>::uninit();
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_mut_ptr().cast::<u8>(), size_of::<T>());
std::ptr::copy_nonoverlapping(
bytes.as_ptr(),
out.as_mut_ptr().cast::<u8>(),
size_of::<T>(),
);
Ok(out.assume_init())
}
}
@@ -292,10 +298,7 @@ fn decode_wire_exact<T: Copy>(bytes: &[u8]) -> Result<T, String> {
#[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>(),
)
std::slice::from_raw_parts((value as *const T).cast::<u8>(), std::mem::size_of::<T>())
}
}
@@ -390,7 +393,10 @@ fn run_redox(json_mode: bool) -> Result<(), 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::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",
@@ -420,14 +426,21 @@ fn run_redox(json_mode: bool) -> Result<(), String> {
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))
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),
&format!(
"allocated exporter GEM handle {} (4096 bytes)",
created.handle
),
));
}
Err(err) => {
@@ -455,24 +468,32 @@ fn run_redox(json_mode: bool) -> Result<(), 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 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))
});
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)) => {
@@ -510,8 +531,12 @@ fn run_redox(json_mode: bool) -> Result<(), String> {
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))
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) => {
@@ -21,48 +21,90 @@ 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 }
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() }
Check {
name: name.to_string(),
result: CheckResult::Pass,
detail: detail.to_string(),
}
}
fn fail(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Fail, detail: detail.to_string() }
Check {
name: name.to_string(),
result: CheckResult::Fail,
detail: detail.to_string(),
}
}
fn skip(name: &str, detail: &str) -> Self {
Check { name: name.to_string(), result: CheckResult::Skip, detail: detail.to_string() }
Check {
name: name.to_string(),
result: CheckResult::Skip,
detail: detail.to_string(),
}
}
}
#[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 {
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 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);
}
@@ -70,33 +112,79 @@ 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 {
drm_device: bool, gpu_firmware: bool, mesa_dri: bool,
display_modes: bool, cs_ioctl: bool, gem_buffers: bool,
hardware_rendering_ready: bool, checks: Vec<JsonCheck>,
drm_device: bool,
gpu_firmware: bool,
mesa_dri: bool,
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,
cs_ioctl,
gem_buffers,
hardware_rendering_ready: hardware_ready,
checks,
}) {
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,
cs_ioctl,
gem_buffers,
hardware_rendering_ready: hardware_ready,
checks,
},
) {
eprintln!("{PROGRAM}: failed to serialize JSON: {err}");
}
}
@@ -142,7 +230,10 @@ fn parse_args() -> Result<bool, String> {
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}")),
}
}
@@ -181,7 +272,10 @@ fn check_gpu_firmware() -> Check {
if found {
Check::pass("GPU_FIRMWARE", "GPU firmware blobs present")
} else {
Check::skip("GPU_FIRMWARE", "no GPU firmware found (may need fetch-firmware.sh)")
Check::skip(
"GPU_FIRMWARE",
"no GPU firmware found (may need fetch-firmware.sh)",
)
}
}
@@ -190,13 +284,28 @@ fn check_mesa_dri_hardware() -> Check {
let hw_drivers = ["/usr/lib/dri/radeonsi_dri.so", "/usr/lib/dri/iris_dri.so"];
let mut found = Vec::new();
for d in hw_drivers {
if std::path::Path::new(d).exists() { found.push(d); }
if std::path::Path::new(d).exists() {
found.push(d);
}
}
if !found.is_empty() {
let names: Vec<_> = found.iter().map(|s| s.rsplit('/').next().unwrap_or(s)).collect();
Check::pass("MESA_DRI", &format!("{} hardware DRI driver(s): {}", found.len(), names.join(", ")))
let names: Vec<_> = found
.iter()
.map(|s| s.rsplit('/').next().unwrap_or(s))
.collect();
Check::pass(
"MESA_DRI",
&format!(
"{} hardware DRI driver(s): {}",
found.len(),
names.join(", ")
),
)
} else {
Check::fail("MESA_DRI", "no hardware DRI drivers found (llvmpipe software only)")
Check::fail(
"MESA_DRI",
"no hardware DRI drivers found (llvmpipe software only)",
)
}
}
@@ -212,7 +321,10 @@ fn check_display_modes() -> Check {
Check::fail("DISPLAY_MODES", "no connectors found")
}
}
Err(_) => Check::skip("DISPLAY_MODES", "cannot enumerate connectors (may need hardware GPU)")
Err(_) => Check::skip(
"DISPLAY_MODES",
"cannot enumerate connectors (may need hardware GPU)",
),
}
}
@@ -230,7 +342,11 @@ fn decode_wire_exact<T: Copy>(bytes: &[u8]) -> Result<T, String> {
let mut out = MaybeUninit::<T>::uninit();
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_mut_ptr().cast::<u8>(), size_of::<T>());
std::ptr::copy_nonoverlapping(
bytes.as_ptr(),
out.as_mut_ptr().cast::<u8>(),
size_of::<T>(),
);
Ok(out.assume_init())
}
}
@@ -238,10 +354,7 @@ fn decode_wire_exact<T: Copy>(bytes: &[u8]) -> Result<T, String> {
#[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>(),
)
std::slice::from_raw_parts((value as *const T).cast::<u8>(), std::mem::size_of::<T>())
}
}
@@ -296,7 +409,10 @@ fn check_gem_buffer_allocation() -> Check {
);
Check::pass(
"GEM_BUFFER_ALLOCATION",
&format!("allocated GEM handle {} over /scheme/drm/card0", created.handle),
&format!(
"allocated GEM handle {} over /scheme/drm/card0",
created.handle
),
)
}
Err(err) => Check::fail("GEM_BUFFER_ALLOCATION", &err),
@@ -429,7 +545,10 @@ fn check_hardware_rendering_ready(report: &Report) -> Check {
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(|a| a == "-h" || a == "--help") {
println!("{USAGE}");
return Err(String::new());
}
println!("{PROGRAM}: GPU check requires Redox runtime");
return Ok(());
}
@@ -446,14 +565,18 @@ fn run() -> Result<(), String> {
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()); }
if report.any_failed() {
return Err("one or more Phase 5 checks failed".to_string());
}
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);
}
+38 -14
View File
@@ -12,7 +12,7 @@
# Exit codes:
# 0 — all checks passed
# 1 — one or more checks failed
# 2 — QEMU boot or login failure
# 2 — QEMU boot/login/runtime infrastructure failure
set -euo pipefail
@@ -65,7 +65,7 @@ run_guest_checks() {
echo
echo "--- IOMMU ---"
run_check "iommu" "redbear-phase-iommu-check" "/scheme/iommu, AMD-Vi/Intel VT-d detection, event log, and interrupt remap setup"
run_check "iommu" "redbear-phase-iommu-check" "AMD-Vi scheme proof or Intel VT-d DMAR detection, event log, and interrupt remap status"
echo
echo "--- DMA ---"
@@ -114,10 +114,24 @@ run_qemu_checks() {
truncate -s 1g "$extra"
fi
expect <<EXPECT_SCRIPT
expect <<EXPECT_SCRIPT
log_user 1
set timeout 300
proc expect_or_exit2 {pattern message} {
expect {
\$pattern { }
timeout {
puts "ERROR: \$message"
exit 2
}
eof {
puts "ERROR: guest exited before \$message"
exit 2
}
}
}
proc run_check {name cmd description ok_marker fail_marker missing_marker} {
global failures
@@ -137,11 +151,11 @@ proc run_check {name cmd description ok_marker fail_marker missing_marker} {
}
timeout {
puts " FAIL \$name: timed out"
incr failures
exit 2
}
eof {
puts " FAIL \$name: guest exited before check completion"
exit 1
exit 2
}
}
puts ""
@@ -149,19 +163,19 @@ proc run_check {name cmd description ok_marker fail_marker missing_marker} {
set failures 0
spawn qemu-system-x86_64 -name {Red Bear OS x86_64} -device qemu-xhci -smp 4 -m 2048 -bios $firmware -chardev stdio,id=debug,signal=off,mux=on -serial chardev:debug -mon chardev=debug -machine q35 -device ich9-intel-hda -device hda-output -device virtio-net,netdev=net0 -netdev user,id=net0 -nographic -vga none -drive file=$image,format=raw,if=none,id=drv0 -device nvme,drive=drv0,serial=NVME_SERIAL -drive file=$extra,format=raw,if=none,id=drv1 -device nvme,drive=drv1,serial=NVME_EXTRA -enable-kvm -cpu host
expect "login:"
expect_or_exit2 "login:" "login prompt"
send "root\r"
expect "assword:"
expect_or_exit2 "assword:" "password prompt"
send "password\r"
expect "Type 'help' for available commands."
expect_or_exit2 "Type 'help' for available commands." "interactive shell prompt"
send "echo __READY__\r"
expect "__READY__"
expect_or_exit2 "__READY__" "guest readiness marker"
puts "=== Red Bear OS IRQ Runtime Validation ==="
puts ""
run_check "PCI IRQ" "redbear-phase-pci-irq-check" "/scheme/irq, MSI/MSI-X capability, affinity, and spurious IRQ routing quality" "__PCI_IRQ_OK__" "__PCI_IRQ_FAIL__" "__PCI_IRQ_MISSING__"
run_check "IOMMU" "redbear-phase-iommu-check" "/scheme/iommu, AMD-Vi/Intel VT-d detection, event log, and interrupt remap setup" "__IOMMU_OK__" "__IOMMU_FAIL__" "__IOMMU_MISSING__"
run_check "IOMMU" "redbear-phase-iommu-check" "AMD-Vi scheme proof or Intel VT-d DMAR detection, event log, and interrupt remap status" "__IOMMU_OK__" "__IOMMU_FAIL__" "__IOMMU_MISSING__"
run_check "DMA" "redbear-phase-dma-check" "DMA buffer allocation and write/readback" "__DMA_OK__" "__DMA_FAIL__" "__DMA_MISSING__"
run_check "PS/2 + serio" "redbear-phase-ps2-check" "/scheme/input/ps2 or serio runtime path" "__PS2_OK__" "__PS2_FAIL__" "__PS2_MISSING__"
run_check "monotonic timer" "redbear-phase-timer-check" "/scheme/time/CLOCK_MONOTONIC monotonic progress" "__TIMER_OK__" "__TIMER_FAIL__" "__TIMER_MISSING__"
@@ -175,13 +189,23 @@ if {\$failures == 0} {
}
send "echo __IRQ_RUNTIME_DONE__\$failures__\r"
expect "__IRQ_RUNTIME_DONE__\$failures__"
expect_or_exit2 "__IRQ_RUNTIME_DONE__\$failures__" "final IRQ runtime summary marker"
set final_status 0
if {\$failures != 0} {
exit 1
set final_status 1
}
send "shutdown\r"
expect eof
expect {
eof { }
timeout {
if {\$final_status == 0} {
set final_status 2
}
}
}
exit \$final_status
EXPECT_SCRIPT
}
@@ -200,7 +224,7 @@ QEMU mode boots an image and runs checks automatically.
Required binaries (must be in PATH inside the guest):
redbear-phase-pci-irq-check — PCI IRQ runtime reports, MSI/MSI-X capability, affinity, spurious IRQs
redbear-phase-iommu-check — IOMMU runtime self-test + scheme control probes
redbear-phase-iommu-check — IOMMU runtime self-test + AMD-Vi scheme or Intel VT-d DMAR probes
redbear-phase-dma-check — DMA buffer allocation/runtime proof
redbear-phase-ps2-check — PS/2 + serio runtime proof
redbear-phase-timer-check — monotonic timer runtime proof