From b029ab628fe3ee9f791486848d12f20f7d6a9e90 Mon Sep 17 00:00:00 2001 From: Vasilito Date: Sat, 18 Apr 2026 17:59:10 +0100 Subject: [PATCH] Expand hwutils, udev-shim, and redbear-sessiond system recipes Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../system/redbear-hwutils/recipe.toml | 1 + .../system/redbear-hwutils/source/Cargo.toml | 4 + .../redbear-hwutils/source/src/bin/lspci.rs | 9 +- .../src/bin/redbear-drm-display-check.rs | 631 ++++++++++++++++++ .../src/bin/redbear-phase4-wayland-check.rs | 3 +- .../src/bin/redbear-phase6-kde-check.rs | 2 +- .../system/redbear-hwutils/source/src/lib.rs | 120 ++++ .../source/src/acpi_watcher.rs | 87 +-- .../system/udev-shim/source/Cargo.toml | 1 + .../system/udev-shim/source/src/device_db.rs | 67 +- .../system/udev-shim/source/src/main.rs | 2 +- .../system/udev-shim/source/src/scheme.rs | 4 - 12 files changed, 827 insertions(+), 104 deletions(-) create mode 100644 local/recipes/system/redbear-hwutils/source/src/bin/redbear-drm-display-check.rs diff --git a/local/recipes/system/redbear-hwutils/recipe.toml b/local/recipes/system/redbear-hwutils/recipe.toml index 56ac5fbb..341937f1 100644 --- a/local/recipes/system/redbear-hwutils/recipe.toml +++ b/local/recipes/system/redbear-hwutils/recipe.toml @@ -9,6 +9,7 @@ template = "cargo" "/usr/bin/lsusb" = "lsusb" "/usr/bin/redbear-usb-check" = "redbear-usb-check" "/usr/bin/redbear-bluetooth-battery-check" = "redbear-bluetooth-battery-check" +"/usr/bin/redbear-drm-display-check" = "redbear-drm-display-check" "/usr/bin/redbear-phase4-wayland-check" = "redbear-phase4-wayland-check" "/usr/bin/redbear-phase5-network-check" = "redbear-phase5-network-check" "/usr/bin/redbear-phase5-wifi-check" = "redbear-phase5-wifi-check" diff --git a/local/recipes/system/redbear-hwutils/source/Cargo.toml b/local/recipes/system/redbear-hwutils/source/Cargo.toml index 37a1ea3f..7d42dbcf 100644 --- a/local/recipes/system/redbear-hwutils/source/Cargo.toml +++ b/local/recipes/system/redbear-hwutils/source/Cargo.toml @@ -59,6 +59,10 @@ path = "src/bin/redbear-phase5-wifi-link-check.rs" name = "redbear-phase6-kde-check" path = "src/bin/redbear-phase6-kde-check.rs" +[[bin]] +name = "redbear-drm-display-check" +path = "src/bin/redbear-drm-display-check.rs" + [[bin]] name = "redbear-phase-iommu-check" path = "src/bin/redbear-phase-iommu-check.rs" diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs b/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs index f3c894b5..be18d28f 100644 --- a/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs +++ b/local/recipes/system/redbear-hwutils/source/src/bin/lspci.rs @@ -1,7 +1,9 @@ use std::fs; use std::process; -use redbear_hwutils::{parse_args, parse_pci_location, PciLocation}; +use redbear_hwutils::{ + lookup_pci_device_name, lookup_pci_vendor_name, parse_args, parse_pci_location, PciLocation, +}; use redox_driver_sys::pci::PciDeviceInfo; use redox_driver_sys::quirks::{lookup_pci_quirks, PciQuirkFlags}; @@ -153,6 +155,11 @@ fn run() -> Result<(), String> { device.device_id, device.revision, ); + if let Some(device_name) = lookup_pci_device_name(device.vendor_id, device.device_id) { + print!(" ({device_name})"); + } else if let Some(vendor_name) = lookup_pci_vendor_name(device.vendor_id) { + print!(" ({vendor_name})"); + } if !device.quirk_flags.is_empty() { print!(" quirks: {}", format_quirk_flags(device.quirk_flags)); } diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-drm-display-check.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-drm-display-check.rs new file mode 100644 index 00000000..fd3ed195 --- /dev/null +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-drm-display-check.rs @@ -0,0 +1,631 @@ +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::mem::{size_of, MaybeUninit}; +use std::path::Path; +use std::process::{self}; + +const PROGRAM: &str = "redbear-drm-display-check"; +const USAGE: &str = "Usage: redbear-drm-display-check --vendor amd|intel [--card /scheme/drm/card0] [--modeset CONNECTOR:MODE]\n\nBounded DRM/KMS display validation checker. This proves only display-path evidence, not render proof."; + +const DRM_IOCTL_BASE: usize = 0x00A0; +const DRM_IOCTL_MODE_GETRESOURCES: usize = DRM_IOCTL_BASE; +const DRM_IOCTL_MODE_SETCRTC: usize = DRM_IOCTL_BASE + 2; +const DRM_IOCTL_MODE_GETCRTC: usize = DRM_IOCTL_BASE + 3; +const DRM_IOCTL_MODE_GETENCODER: usize = DRM_IOCTL_BASE + 6; +const DRM_IOCTL_MODE_GETCONNECTOR: usize = DRM_IOCTL_BASE + 7; +const DRM_IOCTL_MODE_GETMODES: usize = DRM_IOCTL_BASE + 8; +const DRM_IOCTL_MODE_CREATE_DUMB: usize = DRM_IOCTL_BASE + 18; +const DRM_IOCTL_MODE_DESTROY_DUMB: usize = DRM_IOCTL_BASE + 20; +const DRM_IOCTL_MODE_ADDFB: usize = DRM_IOCTL_BASE + 21; +const DRM_IOCTL_MODE_RMFB: usize = DRM_IOCTL_BASE + 22; + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmResourcesWire { + connector_count: u32, + crtc_count: u32, + encoder_count: u32, +} + +#[derive(Clone, Debug)] +struct ResourcesSummary { + connector_count: u32, + connector_ids: Vec, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmConnectorWire { + connector_id: u32, + connection: u32, + connector_type: u32, + mm_width: u32, + mm_height: u32, + encoder_id: u32, + mode_count: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmModeWire { + clock: u32, + hdisplay: u16, + hsync_start: u16, + hsync_end: u16, + htotal: u16, + hskew: u16, + vdisplay: u16, + vsync_start: u16, + vsync_end: u16, + vtotal: u16, + vscan: u16, + vrefresh: u32, + flags: u32, + type_: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmSetCrtcWire { + crtc_id: u32, + fb_handle: u32, + connector_count: u32, + connectors: [u32; 8], + mode: DrmModeWire, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmCreateDumbWire { + width: u32, + height: u32, + bpp: u32, + flags: u32, + pitch: u32, + size: u64, + handle: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmDestroyDumbWire { + handle: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmGetEncoderWire { + encoder_id: u32, + encoder_type: u32, + crtc_id: u32, + possible_crtcs: u32, + possible_clones: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmAddFbWire { + width: u32, + height: u32, + pitch: u32, + bpp: u32, + depth: u32, + handle: u32, + fb_id: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmRmFbWire { + fb_id: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default)] +struct DrmGetCrtcWire { + crtc_id: u32, + fb_id: u32, + x: u32, + y: u32, + mode_valid: u32, + mode: DrmModeWire, +} + +#[derive(Clone, Debug)] +struct ModeSummary { + wire: DrmModeWire, + name: String, +} + +#[derive(Clone, Debug)] +struct ConnectorSummary { + id: u32, + mode_count: u32, + encoder_id: u32, +} + +fn require_path(path: &str, label: &str) -> Result<(), String> { + if Path::new(path).exists() { + println!("{label}=ok"); + Ok(()) + } else { + Err(format!("{label}=missing")) + } +} + +fn parse_args() -> Result<(String, String, Option), String> { + let mut vendor = None; + let mut card = "/scheme/drm/card0".to_string(); + let mut modeset = None; + + let mut args = std::env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "--vendor" => vendor = args.next(), + "--card" => card = args.next().ok_or_else(|| "missing value for --card".to_string())?, + "--modeset" => { + modeset = Some(args.next().ok_or_else(|| "missing value for --modeset".to_string())?) + } + "-h" | "--help" => { + println!("{USAGE}"); + process::exit(0); + } + _ => return Err(format!("unsupported argument: {arg}")), + } + } + + let vendor = vendor.ok_or_else(|| "missing --vendor amd|intel".to_string())?; + if vendor != "amd" && vendor != "intel" { + return Err(format!("unsupported vendor '{vendor}'")); + } + + Ok((vendor, card, modeset)) +} + +fn decode_wire(bytes: &[u8]) -> Result { + if bytes.len() < size_of::() { + return Err(format!( + "short DRM response: expected {} bytes, got {}", + size_of::(), + bytes.len() + )); + } + let mut out = MaybeUninit::::uninit(); + unsafe { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.as_mut_ptr().cast::(), size_of::()); + Ok(out.assume_init()) + } +} + +fn bytes_of(value: &T) -> &[u8] { + unsafe { std::slice::from_raw_parts((value as *const T).cast::(), size_of::()) } +} + +fn open_drm_card(card_path: &str) -> Result { + OpenOptions::new() + .read(true) + .write(true) + .open(card_path) + .map_err(|err| format!("failed to open {card_path}: {err}")) +} + +fn drm_query(file: &mut File, request: usize, payload: &[u8]) -> Result, String> { + 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; 8192]; + let len = file + .read(&mut response) + .map_err(|err| format!("failed to read DRM ioctl {request:#x} response: {err}"))?; + response.truncate(len); + Ok(response) +} + +fn query_empty(file: &mut File, request: usize, payload: &[u8]) -> Result<(), String> { + let response = drm_query(file, request, payload)?; + if response == [0] || response.is_empty() { + Ok(()) + } else { + Err(format!("unexpected non-empty response for ioctl {request:#x}")) + } +} + +fn query_resources(file: &mut File) -> Result { + let response = drm_query(file, DRM_IOCTL_MODE_GETRESOURCES, &[])?; + let header = decode_wire::(&response)?; + let mut connector_ids = Vec::new(); + let mut offset = size_of::(); + for _ in 0..header.connector_count { + if response.len() < offset + size_of::() { + return Err("resources response missing connector id payload".to_string()); + } + connector_ids.push(decode_wire::(&response[offset..offset + size_of::()])?); + offset += size_of::(); + } + + Ok(ResourcesSummary { + connector_count: header.connector_count, + connector_ids, + }) +} + +fn query_connector(file: &mut File, connector_id: u32) -> Result { + let response = drm_query(file, DRM_IOCTL_MODE_GETCONNECTOR, &connector_id.to_le_bytes())?; + decode_wire(&response) +} + +fn query_modes(file: &mut File, connector_id: u32) -> Result, String> { + let response = drm_query(file, DRM_IOCTL_MODE_GETMODES, &connector_id.to_le_bytes())?; + if response == [0] { + return Ok(Vec::new()); + } + + let mode_size = size_of::(); + let mut modes = Vec::new(); + let mut offset = 0usize; + + while offset < response.len() { + if response.len() - offset < mode_size { + return Err(format!( + "truncated mode response: {} trailing bytes left", + response.len() - offset + )); + } + + let mode = decode_wire::(&response[offset..offset + mode_size])?; + offset += mode_size; + + let name_bytes = &response[offset..]; + let Some(name_len) = name_bytes.iter().position(|byte| *byte == 0) else { + return Err("mode response missing trailing NUL after mode name".to_string()); + }; + let name = String::from_utf8_lossy(&name_bytes[..name_len]).to_string(); + offset += name_len + 1; + modes.push(ModeSummary { wire: mode, name }); + } + + Ok(modes) +} + +fn enumerate_connectors(file: &mut File) -> Result, String> { + let resources = query_resources(file)?; + let mut found = Vec::new(); + + for connector_id in resources.connector_ids { + let Ok(connector) = query_connector(file, connector_id) else { + continue; + }; + found.push(ConnectorSummary { + id: connector.connector_id, + mode_count: connector.mode_count, + encoder_id: connector.encoder_id, + }); + } + + if found.is_empty() && resources.connector_count != 0 { + return Err("DRM_CONNECTOR_ENUM=missing".to_string()); + } + + Ok(found) +} + +fn query_encoder(file: &mut File, encoder_id: u32) -> Result { + let response = drm_query(file, DRM_IOCTL_MODE_GETENCODER, &encoder_id.to_le_bytes())?; + decode_wire(&response) +} + +fn query_addfb(file: &mut File, request: &DrmAddFbWire) -> Result { + let response = drm_query(file, DRM_IOCTL_MODE_ADDFB, bytes_of(request))?; + decode_wire(&response) +} + +fn query_create_dumb(file: &mut File, request: &DrmCreateDumbWire) -> Result { + let response = drm_query(file, DRM_IOCTL_MODE_CREATE_DUMB, bytes_of(request))?; + decode_wire(&response) +} + +fn query_get_crtc(file: &mut File, request: &DrmGetCrtcWire) -> Result { + let response = drm_query(file, DRM_IOCTL_MODE_GETCRTC, bytes_of(request))?; + decode_wire(&response) +} + +fn find_mode<'a>(modes: &'a [ModeSummary], name: &str) -> Option<&'a ModeSummary> { + modes.iter().find(|mode| mode.name == name) +} + +fn parse_modeset_spec(spec: &str) -> Result<(u32, &str), String> { + let (connector_text, mode_name) = spec + .split_once(':') + .ok_or_else(|| "--modeset must be CONNECTOR:MODE".to_string())?; + let connector_id = connector_text + .parse::() + .map_err(|err| format!("invalid connector id '{connector_text}': {err}"))?; + Ok((connector_id, mode_name)) +} + +fn disable_crtc_request(crtc_id: u32) -> DrmSetCrtcWire { + DrmSetCrtcWire { + crtc_id, + fb_handle: 0, + connector_count: 0, + connectors: [0; 8], + mode: DrmModeWire::default(), + } +} + +fn setcrtc_request(crtc_id: u32, connector_id: u32, fb_id: u32, mode: DrmModeWire) -> DrmSetCrtcWire { + let mut request = DrmSetCrtcWire { + crtc_id, + fb_handle: fb_id, + connector_count: 1, + connectors: [0; 8], + mode, + }; + request.connectors[0] = connector_id; + request +} + +fn proof_teardown_requests( + crtc_id: u32, + fb_id: u32, + gem_handle: u32, +) -> (DrmSetCrtcWire, DrmRmFbWire, DrmDestroyDumbWire) { + ( + disable_crtc_request(crtc_id), + DrmRmFbWire { fb_id }, + DrmDestroyDumbWire { handle: gem_handle }, + ) +} + +fn bounded_modeset_proof( + file: &mut File, + connectors: &[ConnectorSummary], + spec: &str, +) -> Result<(), String> { + let (connector_id, mode_name) = parse_modeset_spec(spec)?; + + let connector = connectors + .iter() + .find(|connector| connector.id == connector_id) + .ok_or_else(|| format!("connector {connector_id} not found in enumeration results"))?; + + let modes = query_modes(file, connector_id)?; + let mode = find_mode(&modes, mode_name) + .ok_or_else(|| format!("mode '{mode_name}' not found on connector {connector_id}"))?; + + let encoder = query_encoder(file, connector.encoder_id)?; + let crtc_id = encoder.crtc_id; + if crtc_id == 0 { + return Err(format!("connector {connector_id} encoder did not report a usable CRTC")); + } + + let create = query_create_dumb( + file, + &DrmCreateDumbWire { + width: mode.wire.hdisplay as u32, + height: mode.wire.vdisplay as u32, + bpp: 32, + ..DrmCreateDumbWire::default() + }, + )?; + let addfb = query_addfb( + file, + &DrmAddFbWire { + width: mode.wire.hdisplay as u32, + height: mode.wire.vdisplay as u32, + pitch: create.pitch, + bpp: 32, + depth: 24, + handle: create.handle, + ..DrmAddFbWire::default() + }, + )?; + + let setcrtc = setcrtc_request(crtc_id, connector_id, addfb.fb_id, mode.wire); + query_empty(file, DRM_IOCTL_MODE_SETCRTC, bytes_of(&setcrtc))?; + + let getcrtc = query_get_crtc( + file, + &DrmGetCrtcWire { + crtc_id, + ..DrmGetCrtcWire::default() + }, + )?; + if getcrtc.fb_id != addfb.fb_id || getcrtc.mode_valid == 0 { + return Err("GETCRTC did not confirm the programmed framebuffer/mode".to_string()); + } + + let (disable, rmfb, destroy) = proof_teardown_requests(crtc_id, addfb.fb_id, create.handle); + query_empty(file, DRM_IOCTL_MODE_SETCRTC, bytes_of(&disable))?; + query_empty(file, DRM_IOCTL_MODE_RMFB, bytes_of(&rmfb))?; + query_empty(file, DRM_IOCTL_MODE_DESTROY_DUMB, bytes_of(&destroy))?; + + Ok(()) +} + +#[cfg(test)] +fn has_connector_section(text: &str) -> bool { + text.contains("Connectors:") +} + +#[cfg(test)] +fn has_mode_lines(text: &str) -> bool { + text.lines().any(|line| { + let trimmed = line.trim_start(); + let mut parts = trimmed.split_whitespace(); + matches!( + (parts.next(), parts.next()), + (Some(id), Some(mode)) if id.chars().all(|c| c.is_ascii_digit()) && mode.contains('x') + ) + }) +} + +fn run() -> Result<(), String> { + let (vendor, card_path, modeset) = parse_args()?; + + println!("=== Red Bear DRM Display Runtime Check ==="); + println!("DRM_VENDOR={vendor}"); + println!("DRM_CARD={card_path}"); + + require_path("/scheme/drm", "DRM_SCHEME")?; + require_path(&card_path, "DRM_CARD_NODE")?; + + let mut drm = open_drm_card(&card_path)?; + let connectors = enumerate_connectors(&mut drm)?; + println!("DRM_CONNECTOR_ENUM=ok"); + + let mut mode_lines_found = false; + for connector in &connectors { + let modes = query_modes(&mut drm, connector.id)?; + if !modes.is_empty() && modes.len() as u32 == connector.mode_count { + mode_lines_found = true; + break; + } + } + if !mode_lines_found { + return Err("DRM_MODE_ENUM=missing".to_string()); + } + println!("DRM_MODE_ENUM=ok"); + println!("DRM_ENUMERATION=ok"); + + if let Some(spec) = modeset { + if let Err(err) = bounded_modeset_proof(&mut drm, &connectors, &spec) { + println!("DRM_MODESET_PROOF=failed"); + println!("DRM_MODESET_SPEC={spec}"); + return Err(err); + } + println!("DRM_MODESET_PROOF=ok"); + println!("DRM_MODESET_SPEC={spec}"); + } else { + println!("DRM_MODESET_PROOF=skipped_no_spec"); + } + + println!("DRM_RENDER_PROOF=not_attempted"); + println!("DRM_TRANCHE_SUMMARY=display_validation_only"); + Ok(()) +} + +fn main() { + if let Err(err) = run() { + eprintln!("{PROGRAM}: {err}"); + process::exit(1); + } +} + +#[cfg(test)] +mod tests { + use super::{bytes_of, decode_wire, disable_crtc_request, find_mode, has_connector_section, has_mode_lines, parse_modeset_spec, proof_teardown_requests, setcrtc_request, DrmModeWire, DrmResourcesWire, ModeSummary}; + + fn owned_bytes_of(value: &T) -> Vec { + unsafe { + std::slice::from_raw_parts((value as *const T).cast::(), size_of::()).to_vec() + } + } + + #[test] + fn connector_section_detected() { + assert!(has_connector_section("foo\nConnectors:\nbar")); + } + + #[test] + fn mode_lines_detected() { + assert!(has_mode_lines(" 42 1920x1080 60.00")); + assert!(!has_mode_lines("Connectors:\nnone")); + } + + #[test] + fn query_modes_accepts_empty_sentinel() { + let parsed = if vec![0] == [0] { Vec::::new() } else { unreachable!() }; + assert!(parsed.is_empty()); + } + + #[test] + fn resources_header_plus_connector_ids_round_trip() { + let header = DrmResourcesWire { + connector_count: 2, + crtc_count: 1, + encoder_count: 2, + }; + let mut payload = owned_bytes_of(&header); + payload.extend_from_slice(&1u32.to_ne_bytes()); + payload.extend_from_slice(&7u32.to_ne_bytes()); + + let decoded = decode_wire::(&payload).unwrap(); + assert_eq!(decoded.connector_count, 2); + let first = decode_wire::(&payload[size_of::()..]).unwrap(); + let second = decode_wire::(&payload[size_of::() + 4..]).unwrap(); + assert_eq!(first, 1); + assert_eq!(second, 7); + } + + #[test] + fn mode_wire_decode_round_trip_works() { + let mode = DrmModeWire { + hdisplay: 1920, + vdisplay: 1080, + vrefresh: 60, + ..DrmModeWire::default() + }; + let decoded = decode_wire::(bytes_of(&mode)).unwrap(); + assert_eq!(decoded.hdisplay, 1920); + assert_eq!(decoded.vdisplay, 1080); + assert_eq!(decoded.vrefresh, 60); + } + + #[test] + fn find_mode_matches_by_name() { + let modes = vec![ModeSummary { + wire: DrmModeWire::default(), + name: "1920x1080@60".to_string(), + }]; + + assert!(find_mode(&modes, "1920x1080@60").is_some()); + assert!(find_mode(&modes, "1280x720@60").is_none()); + } + + #[test] + fn parse_modeset_spec_accepts_connector_and_mode() { + let (connector, mode) = parse_modeset_spec("7:1920x1080@60").unwrap(); + + assert_eq!(connector, 7); + assert_eq!(mode, "1920x1080@60"); + } + + #[test] + fn parse_modeset_spec_rejects_bad_shape() { + assert!(parse_modeset_spec("broken-spec").is_err()); + } + + #[test] + fn disable_crtc_request_zeroes_active_state() { + let request = disable_crtc_request(3); + + assert_eq!(request.crtc_id, 3); + assert_eq!(request.fb_handle, 0); + assert_eq!(request.connector_count, 0); + assert!(request.connectors.iter().all(|&value| value == 0)); + } + + #[test] + fn setcrtc_request_targets_single_connector_and_fb() { + let request = setcrtc_request(3, 7, 11, DrmModeWire::default()); + + assert_eq!(request.crtc_id, 3); + assert_eq!(request.fb_handle, 11); + assert_eq!(request.connector_count, 1); + assert_eq!(request.connectors[0], 7); + } + + #[test] + fn proof_teardown_requests_disable_then_release_resources() { + let (disable, rmfb, destroy) = proof_teardown_requests(3, 11, 22); + + assert_eq!(disable.crtc_id, 3); + assert_eq!(disable.fb_handle, 0); + assert_eq!(disable.connector_count, 0); + assert_eq!(rmfb.fb_id, 11); + assert_eq!(destroy.handle, 22); + } +} diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase4-wayland-check.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase4-wayland-check.rs index a3f19d30..ed402e0d 100644 --- a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase4-wayland-check.rs +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase4-wayland-check.rs @@ -60,9 +60,8 @@ fn run() -> Result<(), String> { })?; println!("=== Red Bear OS Phase 4 Wayland Runtime Check ==="); - require_path("/usr/bin/orbital-wayland")?; + require_path("/usr/bin/redbear-validation-session")?; require_path("/usr/bin/wayland-session")?; - require_path("/usr/bin/smallvil")?; require_path("/usr/bin/qt6-bootstrap-check")?; require_path("/usr/bin/qt6-plugin-check")?; require_path("/usr/bin/qt6-wayland-smoke")?; diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase6-kde-check.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase6-kde-check.rs index 3a0acb7a..dea3cd6d 100644 --- a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase6-kde-check.rs +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase6-kde-check.rs @@ -204,7 +204,7 @@ fn run() -> Result<(), String> { })?; println!("=== Red Bear OS Phase 6 KDE Runtime Check ==="); - require_path("/usr/bin/orbital-kde")?; + require_path("/usr/bin/redbear-kde-session")?; require_path("/usr/bin/kwin_wayland")?; require_path("/usr/bin/dbus-daemon")?; require_path("/usr/bin/seatd")?; diff --git a/local/recipes/system/redbear-hwutils/source/src/lib.rs b/local/recipes/system/redbear-hwutils/source/src/lib.rs index 4125771c..745ff68a 100644 --- a/local/recipes/system/redbear-hwutils/source/src/lib.rs +++ b/local/recipes/system/redbear-hwutils/source/src/lib.rs @@ -1,4 +1,17 @@ +use std::collections::HashMap; use std::fmt; +use std::fs; +use std::sync::OnceLock; + +const PCI_IDS_PATH: &str = "/usr/share/misc/pci.ids"; + +#[derive(Default)] +struct PciIdDatabase { + vendor_names: HashMap, + device_names: HashMap<(u16, u16), String>, +} + +static PCI_ID_DATABASE: OnceLock> = OnceLock::new(); #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct PciLocation { @@ -78,3 +91,110 @@ pub fn describe_usb_device(manufacturer: Option<&str>, product: Option<&str>) -> parts.join(" ") } } + +fn load_pci_id_database() -> Option { + let text = fs::read_to_string(PCI_IDS_PATH).ok()?; + Some(parse_pci_id_database(&text)) +} + +fn parse_pci_id_database(text: &str) -> PciIdDatabase { + let mut database = PciIdDatabase::default(); + let mut current_vendor = None; + + for line in text.lines() { + if line.is_empty() || line.starts_with('#') { + continue; + } + + if let Some(rest) = line.strip_prefix("\t\t") { + let _ = rest; + continue; + } + + if let Some(rest) = line.strip_prefix('\t') { + let Some(vendor_id) = current_vendor else { + continue; + }; + let mut parts = rest.splitn(2, char::is_whitespace).filter(|part| !part.is_empty()); + let Some(device_hex) = parts.next() else { + continue; + }; + let Some(name) = parts.next() else { + continue; + }; + let Ok(device_id) = u16::from_str_radix(device_hex, 16) else { + continue; + }; + database + .device_names + .insert((vendor_id, device_id), name.trim().to_string()); + continue; + } + + let mut parts = line.splitn(2, char::is_whitespace).filter(|part| !part.is_empty()); + let Some(vendor_hex) = parts.next() else { + continue; + }; + let Some(name) = parts.next() else { + continue; + }; + let Ok(vendor_id) = u16::from_str_radix(vendor_hex, 16) else { + continue; + }; + current_vendor = Some(vendor_id); + database.vendor_names.insert(vendor_id, name.trim().to_string()); + } + + database +} + +fn pci_id_database() -> Option<&'static PciIdDatabase> { + PCI_ID_DATABASE.get_or_init(load_pci_id_database).as_ref() +} + +pub fn lookup_pci_vendor_name(vendor_id: u16) -> Option { + pci_id_database()?.vendor_names.get(&vendor_id).cloned() +} + +pub fn lookup_pci_device_name(vendor_id: u16, device_id: u16) -> Option { + pci_id_database()? + .device_names + .get(&(vendor_id, device_id)) + .cloned() +} + +#[cfg(test)] +mod tests { + use super::parse_pci_id_database; + + #[test] + fn parses_vendor_and_device_entries_from_pci_ids() { + let db = parse_pci_id_database( + "8086 Intel Corporation\n\t46A6 Alder Lake-P Integrated Graphics Controller\n1002 Advanced Micro Devices, Inc. [AMD/ATI]\n\t7480 Navi 32 [Radeon RX 7800 XT / 7700 XT]\n", + ); + + assert_eq!( + db.vendor_names.get(&0x8086).map(String::as_str), + Some("Intel Corporation") + ); + assert_eq!( + db.device_names.get(&(0x8086, 0x46A6)).map(String::as_str), + Some("Alder Lake-P Integrated Graphics Controller") + ); + assert_eq!( + db.device_names.get(&(0x1002, 0x7480)).map(String::as_str), + Some("Navi 32 [Radeon RX 7800 XT / 7700 XT]") + ); + } + + #[test] + fn ignores_subsystem_lines_and_comments() { + let db = parse_pci_id_database( + "# comment\n8086 Intel Corporation\n\t46A6 Alder Lake-P Integrated Graphics Controller\n\t\t17AA 3C6A Lenovo variant\n", + ); + + assert_eq!(db.vendor_names.len(), 1); + assert_eq!(db.device_names.len(), 1); + assert!(db.device_names.get(&(0x17AA, 0x3C6A)).is_none()); + } +} diff --git a/local/recipes/system/redbear-sessiond/source/src/acpi_watcher.rs b/local/recipes/system/redbear-sessiond/source/src/acpi_watcher.rs index fd978d50..7dc962d9 100644 --- a/local/recipes/system/redbear-sessiond/source/src/acpi_watcher.rs +++ b/local/recipes/system/redbear-sessiond/source/src/acpi_watcher.rs @@ -1,67 +1,42 @@ -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::Duration; use zbus::Connection; -static SLEEP_ACTIVE: AtomicBool = AtomicBool::new(false); -static SHUTDOWN_FIRED: AtomicBool = AtomicBool::new(false); +#[cfg(target_os = "redox")] +const KSTOP_PATH: &str = "/scheme/kernel.acpi/kstop"; -const ACPI_SLEEP_PATH: &str = "/scheme/acpi/sleep"; -const ACPI_SHUTDOWN_PATH: &str = "/scheme/acpi/shutdown"; -const POLL_INTERVAL: Duration = Duration::from_secs(5); +#[cfg(target_os = "redox")] +fn wait_for_shutdown_edge() -> std::io::Result<()> { + use std::io::Read; -fn read_acpi_flag(path: &str) -> bool { - match std::fs::read_to_string(path) { - Ok(content) => { - let trimmed = content.trim().to_lowercase(); - !trimmed.is_empty() && trimmed != "0" - } - Err(_) => false, - } + let mut file = std::fs::File::open(KSTOP_PATH)?; + let mut byte = [0_u8; 1]; + let _ = file.read(&mut byte)?; + Ok(()) } pub async fn watch_and_emit(connection: Connection) { - loop { - tokio::time::sleep(POLL_INTERVAL).await; - - let sleep_now = tokio::task::spawn_blocking(|| read_acpi_flag(ACPI_SLEEP_PATH)) - .await - .unwrap_or(false); - - let was_sleeping = SLEEP_ACTIVE.load(Ordering::Relaxed); - - if sleep_now && !was_sleeping { - SLEEP_ACTIVE.store(true, Ordering::Relaxed); - let _ = connection.emit_signal( - None::<&str>, - "/org/freedesktop/login1", - "org.freedesktop.login1.Manager", - "PrepareForSleep", - &true, - ).await; - } else if !sleep_now && was_sleeping { - SLEEP_ACTIVE.store(false, Ordering::Relaxed); - let _ = connection.emit_signal( - None::<&str>, - "/org/freedesktop/login1", - "org.freedesktop.login1.Manager", - "PrepareForSleep", - &false, - ).await; + #[cfg(target_os = "redox")] + match tokio::task::spawn_blocking(wait_for_shutdown_edge).await { + Ok(Ok(())) => { + let _ = connection + .emit_signal( + None::<&str>, + "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", + "PrepareForShutdown", + &true, + ) + .await; } - - let shutdown_now = tokio::task::spawn_blocking(|| read_acpi_flag(ACPI_SHUTDOWN_PATH)) - .await - .unwrap_or(false); - - if shutdown_now && !SHUTDOWN_FIRED.load(Ordering::Relaxed) { - SHUTDOWN_FIRED.store(true, Ordering::Relaxed); - let _ = connection.emit_signal( - None::<&str>, - "/org/freedesktop/login1", - "org.freedesktop.login1.Manager", - "PrepareForShutdown", - &true, - ).await; + Ok(Err(err)) => { + eprintln!("redbear-sessiond: ACPI shutdown watcher failed: {err}"); + } + Err(err) => { + eprintln!("redbear-sessiond: ACPI shutdown watcher task failed: {err}"); } } + + #[cfg(not(target_os = "redox"))] + { + let _ = connection; + } } diff --git a/local/recipes/system/udev-shim/source/Cargo.toml b/local/recipes/system/udev-shim/source/Cargo.toml index 399533ba..40ac358b 100644 --- a/local/recipes/system/udev-shim/source/Cargo.toml +++ b/local/recipes/system/udev-shim/source/Cargo.toml @@ -10,3 +10,4 @@ redox-scheme = "0.11" syscall = { package = "redox_syscall", version = "0.7", features = ["std"] } log = { version = "0.4", features = ["std"] } thiserror = "2" +redbear-hwutils = { path = "../../redbear-hwutils/source" } diff --git a/local/recipes/system/udev-shim/source/src/device_db.rs b/local/recipes/system/udev-shim/source/src/device_db.rs index c5a0abaa..342cae60 100644 --- a/local/recipes/system/udev-shim/source/src/device_db.rs +++ b/local/recipes/system/udev-shim/source/src/device_db.rs @@ -105,9 +105,14 @@ impl DeviceInfo { } pub fn classify_pci_device(bus: u8, dev: u8, func: u8) -> DeviceInfo { - let devpath = format!("/devices/pci/{:04x}:{:02x}:{:02x}.{}", bus, 0, dev, func); - - let config_path = format!("/scheme/pci/{}.{}.{}", bus, dev, func); + let location = PciLocation { + segment: 0, + bus, + device: dev, + function: func, + }; + let devpath = format!("/devices/pci/{}", location); + let config_path = format!("{}/config", location.scheme_path()); let (vendor_id, device_id, class_code, subclass) = read_pci_config(&config_path); let input_kind = detect_input_kind(class_code, subclass); @@ -174,10 +179,8 @@ fn format_device_name( subclass: u8, input_kind: Option, ) -> String { - if class_code == 0x03 { - if let Some(name) = gpu_device_name(vendor_id, device_id) { - return format!("{name} [{vendor_id:04x}:{device_id:04x}]"); - } + if let Some(name) = lookup_pci_device_name(vendor_id, device_id) { + return format!("{name} [{vendor_id:04x}:{device_id:04x}]"); } if class_code == 0x09 { @@ -189,15 +192,7 @@ fn format_device_name( return format!("{name} [{vendor_id:04x}:{device_id:04x}]"); } - let vendor_name = match vendor_id { - 0x8086 => "Intel", - 0x1002 => "AMD", - 0x10DE => "NVIDIA", - 0x10EC => "Realtek", - 0x8087 => "Intel", - 0x14E4 => "Broadcom", - _ => "Unknown", - }; + let vendor_name = lookup_pci_vendor_name(vendor_id).unwrap_or_else(|| "Unknown".to_string()); let class_name = match class_code { 0x03 => "GPU", @@ -215,29 +210,22 @@ fn format_device_name( ) } -fn gpu_device_name(vendor_id: u16, device_id: u16) -> Option<&'static str> { - match vendor_id { - 0x1002 => match device_id { - 0x73A3 => Some("AMD Radeon RX 6600 XT / 6650 XT (RDNA2)"), - 0x73BF => Some("AMD Radeon RX 6800 XT / 6900 XT (RDNA2)"), - 0x73DF => Some("AMD Radeon RX 6700 XT / 6750 XT (RDNA2)"), - 0x73EF => Some("AMD Radeon RX 6800 / 6850M XT (RDNA2)"), - 0x7422 => Some("AMD Radeon 780M (RDNA3)"), - 0x7448 => Some("AMD Radeon RX 7900 XT (RDNA3)"), - 0x744C => Some("AMD Radeon RX 7900 XTX (RDNA3)"), - 0x7480 => Some("AMD Radeon RX 7800 XT / 7700 XT (RDNA3)"), - _ => Some("AMD Radeon GPU"), - }, - 0x8086 => match device_id { - 0x3E92 => Some("Intel UHD Graphics 630"), - 0x5912 => Some("Intel HD Graphics 630"), - 0x9A49 => Some("Intel Iris Xe Graphics (Tiger Lake)"), - 0x46A6 => Some("Intel Iris Xe Graphics (Alder Lake-P)"), - 0x56A0 => Some("Intel Arc Graphics (DG2)"), - 0x56A1 => Some("Intel Arc A380 (DG2)"), - _ => Some("Intel Graphics"), - }, - _ => None, +#[cfg(test)] +mod tests { + use super::classify_pci_device; + + #[test] + fn classify_pci_device_uses_shared_location_format() { + let device = classify_pci_device(0x02, 0x00, 0x0); + + assert_eq!(device.devpath, "/devices/pci/0000:02:00.0"); + } + + #[test] + fn id_path_tracks_shared_pci_devpath_shape() { + let device = classify_pci_device(0x02, 0x00, 0x0); + + assert_eq!(device.id_path(), "pci-0000:02:00.0"); } } @@ -305,3 +293,4 @@ pub fn format_uevent_info(dev: &DeviceInfo) -> String { } info } +use redbear_hwutils::{lookup_pci_device_name, lookup_pci_vendor_name, PciLocation}; diff --git a/local/recipes/system/udev-shim/source/src/main.rs b/local/recipes/system/udev-shim/source/src/main.rs index b448a061..6d58efda 100644 --- a/local/recipes/system/udev-shim/source/src/main.rs +++ b/local/recipes/system/udev-shim/source/src/main.rs @@ -2,7 +2,7 @@ mod device_db; mod scheme; use std::env; -use std::os::fd::{AsRawFd, FromRawFd, RawFd}; +use std::os::fd::RawFd; use log::{error, info, LevelFilter, Metadata, Record}; use redox_scheme::{ diff --git a/local/recipes/system/udev-shim/source/src/scheme.rs b/local/recipes/system/udev-shim/source/src/scheme.rs index 95f1b981..7045841a 100644 --- a/local/recipes/system/udev-shim/source/src/scheme.rs +++ b/local/recipes/system/udev-shim/source/src/scheme.rs @@ -631,10 +631,6 @@ impl SchemeSync for UdevScheme { } } -fn path_exists(path: &str) -> bool { - std::fs::metadata(path).is_ok() -} - fn scheme_registered(name: &str) -> bool { std::fs::read_dir("/scheme") .ok()