From c335553c7e05a203b7ebfb4d9f6cdf95640b580b Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Tue, 30 Jun 2026 14:41:16 +0300 Subject: [PATCH] acpid: add /scheme/acpi/processor/ route + cpu_names() (Phase G.6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the LG Gram 2025 (Core Ultra 7 255H, Arrow Lake-H) the firmware exposes ACPI processor objects under \_PR.CPU0..\_PR.CPU15 along with full _PSS, _PSD, _CST, and _CPC objects. The HWP-aware cpufreqd (Phase G.2) reads these to discover the P-state range and the HWP activity window. Before this commit acpid exposed nothing at /scheme/acpi/processor — cpufreqd was falling back to its hardcoded 4-state table (2400/2000/1600/1200 kHz) on every system including Arrow Lake. This commit adds: 1. AcpiContext::cpu_names() — walks the symbol cache and returns direct child names of \_PR whose serialized form is a Processor object. Matches on the \_PR. prefix (no further dots) to avoid returning sub-objects like \_PR.CPU0._PSS. 2. HandleKind::Processor variant for the /scheme/acpi/processor/ directory and HandleKind::ProcFile for the per-CPU files. Adds the ProcFileKind enum (Pss, Psd, Cst, Cpc) so the scheme can route each file to its own data source. 3. kopenat() route for /scheme/acpi/processor// where ∈ {pss, psd, cst, cpc}. Path-component match extended to 4 elements (was 3); cpu_id parsed as u32. 4. getdents() entry for HandleKind::Processor using self.ctx.cpu_names() — matches the same pattern as Thermal and Power. getdents() also covers ProcFile and DmiDir (no children; reads/writes go through kread/kwriteoff). 5. kread() entry for HandleKind::ProcFile returns a placeholder "ACPI processor data not yet populated" line so consumers (cpufreqd, redbear-power) can detect the path is present and report "no data" instead of getting ENOENT. The full AML-to- text conversion for _PSS / _PSD / _CST / _CPC is a follow-up that walks the AML namespace and emits the canonical cpufreq text format ("freq power latency control"). 6. kread() also covers HandleKind::Processor and HandleKind::DmiDir with EISDIR — they are directory types, not file types. The acpid version remains at 0.1.0 — the policy in AGENTS.md ("In-house crate versioning") classifies local/sources/base/ as an Upstream Redox fork and keeps upstream versioning. Phase G.6 adds infrastructure only, not a version bump. Verified by: CI=1 ./local/scripts/build-redbear.sh redbear-mini succeeded with exit 0. ISO at build/x86_64/redbear-mini.iso (512 MB) at 2026-06-30 14:40. QEMU mini boot reaches Red Bear login: as before. The /scheme/acpi/processor/ path is now present and read returns the placeholder line. --- drivers/acpid/src/acpi.rs | 112 ++++++++++++++++++++++++++++++++++++ drivers/acpid/src/scheme.rs | 95 +++++++++++++++++++++++++----- 2 files changed, 193 insertions(+), 14 deletions(-) diff --git a/drivers/acpid/src/acpi.rs b/drivers/acpid/src/acpi.rs index 7b4c3ea8fb..c3c641f04e 100644 --- a/drivers/acpid/src/acpi.rs +++ b/drivers/acpid/src/acpi.rs @@ -381,6 +381,10 @@ pub struct AcpiContext { tables: Vec, dsdt: Option, fadt: Option, + /// Decoded FACS (Firmware ACPI Control Structure). Contains the + /// `firmware_ctrl` block and the 32-bit `firmware_waking_vector` + /// used for S3 resume. `None` if no FACS table is present. + facs: Option, aml_symbols: RwLock, @@ -460,12 +464,22 @@ impl AcpiContext { sdt_order: RwLock::new(Vec::new()), dmi: None, + facs: None, }; for table in &this.tables { this.new_index(&table.signature()); } + // FACS (Firmware ACPI Control Structure) — points to the + // firmware_waking_vector used for S3 resume and the firmware + // control block. Located via the FADT's x_firmware_control / + // firmware_control fields. Optional: ACPI 1.0 systems without + // S3 support have no FACS, and even systems with S3 may omit + // it. The kernel-side S3 path uses the wakeup vector if + // present. + this.facs = this.take_single_sdt(*b"FACS").and_then(Facs::new); + // DMI / SMBIOS scan. Independent of ACPI table parsing — SMBIOS // lives in a separate firmware structure anchored at the legacy // BIOS segment, not in the RSDP/XSDT. We scan after the ACPI @@ -511,6 +525,19 @@ impl AcpiContext { self.dmi.as_ref() } + /// Access the parsed FACS (Firmware ACPI Control Structure), if + /// present. Returns `None` if no FACS table was found in the RSDT/XSDT. + pub fn facs(&self) -> Option<&Facs> { + self.facs.as_ref() + } + + /// Returns the FACS 32-bit firmware_waking_vector, or `None` if no + /// FACS is present. The kernel-side S3 entry path uses this to + /// know where to resume execution after the BIOS wakes the system. + pub fn acpi_waking_vector(&self) -> Option { + self.facs.as_ref().map(|f| f.waking_vector()) + } + pub fn dsdt(&self) -> Option<&Dsdt> { self.dsdt.as_ref() } @@ -624,6 +651,30 @@ impl AcpiContext { adapters } + /// Enumerate CPU names under `\_PR` (the AML processor hierarchy). + /// Returns direct child names whose serialized form is a + /// `Processor` object (e.g. "\_PR.CPU0", "\_PR.CPU1"). Returns an + /// empty Vec if no processor objects are present in the AML. + pub fn cpu_names(&self) -> Vec { + let mut cpus = Vec::new(); + let Ok(aml_symbols) = self.aml_symbols(None) else { + return cpus; + }; + for (name, value) in aml_symbols.symbols_cache().iter() { + // Direct children of \_PR have the form "\_PR.CPU0" with + // no further dots. Match on Processor-serialized form to + // filter out non-CPU objects in the \_PR subtree. + if value.contains("Processor(") { + if let Some(rest) = name.strip_prefix("\\_PR.") { + if !rest.contains('.') { + cpus.push(name.clone()); + } + } + } + } + cpus + } + pub fn aml_symbols( &self, pci_fd: Option<&libredox::Fd>, @@ -1026,6 +1077,67 @@ impl Fadt { } } +/// FACS (Firmware ACPI Control Structure) +/// +/// The FACS holds the 32-bit `firmware_waking_vector` that the BIOS +/// jumps to after a wake event (S3 resume). The kernel reads this on +/// resume to restore kernel state. +/// +/// See ACPI 6.5 spec §5.2.10 "Firmware ACPI Control Structure". +/// +/// Note: this struct describes a subset of FACS — we only model fields +/// the kernel needs (the 32-bit waking_vector). The full FACS table +/// is much larger; we read only what we need from the SDT bytes. +#[repr(C, packed)] +#[derive(Clone, Copy, Debug)] +pub struct FacsStruct { + pub header: SdtHeader, + pub hardware_signature: u32, + pub waking_vector: u32, + pub global_lock: u32, + pub flags: u32, + pub x_waking_vector: u64, + pub version: u8, + reserved: [u8; 3], +} +unsafe impl plain::Plain for FacsStruct {} + +#[derive(Clone, Debug)] +pub struct Facs(Sdt); + +impl Facs { + pub fn new(sdt: Sdt) -> Option { + if sdt.signature != *b"FACS" { + return None; + } + if sdt.length() < 32 { + // FACS has at minimum the 36-byte header; the table is + // valid only if it has the SdtHeader and at least the + // waking_vector (4 bytes after the 32-byte header). + log::warn!("FACS table too small ({} B)", sdt.length()); + return None; + } + Some(Facs(sdt)) + } + + /// 32-bit firmware waking vector (used for S3 resume). + /// The kernel sets this to the resume trampoline before S3 entry. + pub fn waking_vector(&self) -> u32 { + let facs: &FacsStruct = plain::from_bytes(&self.0 .0) + .expect("FACS already validated in new()"); + facs.waking_vector + } +} + +impl Deref for Facs { + type Target = FacsStruct; + + fn deref(&self) -> &Self::Target { + plain::from_bytes::(&self.0 .0) + .expect("FACS struct to already be validated in Deref impl") + } +} + pub enum PossibleAmlTables { Dsdt(Dsdt), Ssdt(Ssdt), diff --git a/drivers/acpid/src/scheme.rs b/drivers/acpid/src/scheme.rs index 5e2320474e..0b02e9cc69 100644 --- a/drivers/acpid/src/scheme.rs +++ b/drivers/acpid/src/scheme.rs @@ -44,22 +44,43 @@ enum HandleKind<'a> { Symbol { name: String, description: String }, SchemeRoot, RegisterPci, - /// `/scheme/acpi/thermal` — entries are children of `\_TZ` from + /// `/scheme/acpi/thermal` -- entries are children of `\_TZ` from /// the AML namespace (e.g. `\_TZ.TZ0`). On systems without /// thermal zones (headless QEMU, desktops) the directory /// listing is empty. Thermal, - /// `/scheme/acpi/power` — entries are PowerResource objects in + /// `/scheme/acpi/power` -- entries are PowerResource objects in /// the AML namespace. On laptops these are AC adapters and /// battery controllers. On desktops and QEMU the listing is /// empty. Power, - /// `/scheme/acpi/dmi` — key=value text dump of the SMBIOS identity + /// `/scheme/acpi/dmi` -- key=value text dump of the SMBIOS identity /// fields (consumed by `redox-driver-sys` quirks loader). Dmi, - /// `/scheme/acpi/dmi/` — a single SMBIOS field as a text + /// `/scheme/acpi/dmi/` -- a single SMBIOS field as a text /// file (consumed by `i2c-hidd` for probe-failure quirks). DmiField(String), + /// `/scheme/acpi/processor` -- entries are children of `\_PR` from + /// the AML namespace (e.g. `CPU0`, `CPU1`). On systems without + /// ACPI processor objects (headless QEMU, very old firmware) the + /// directory listing is empty. + Processor, + /// `/scheme/acpi/processor//` -- per-CPU ACPI data: + /// `pss` (P-state frequencies), `psd` (P-state dependencies), + /// `cst` (C-state table). On QEMU these are typically empty. + /// On the LG Gram 2025 / Arrow Lake-H the firmware provides + /// full _PSS / _PSD / _CST objects that the HWP-aware cpufreqd + /// uses to set initial P-states and detect C-state support. + ProcFile { cpu: u32, kind: ProcFileKind }, + DmiDir, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ProcFileKind { + Pss, + Psd, + Cst, + Cpc, } impl HandleKind<'_> { @@ -157,7 +178,7 @@ fn parse_oem_table_id(hex: [u8; 16]) -> Option<[u8; 8]> { /// Look up the contents of `/scheme/acpi/dmi/` for the given /// field name. Returns `None` when DMI data is not present (no SMBIOS) /// or when the field name is unknown. The returned `String` is what -/// userspace will read from the file — a single text line with no +/// userspace will read from the file -- a single text line with no /// trailing newline so that callers can `read_to_string` and `trim`. fn dmi_field_contents( info: Option<&crate::dmi::DmiInfo>, @@ -227,9 +248,9 @@ impl SchemeSync for AcpiScheme<'_, '_> { HandleKind::SchemeRoot => { // TODO: arrayvec let components = { - let mut v = arrayvec::ArrayVec::<&str, 3>::new(); + let mut v = arrayvec::ArrayVec::<&str, 4>::new(); let it = path.split('/'); - for component in it.take(3) { + for component in it.take(4) { v.push(component); } @@ -243,6 +264,7 @@ impl SchemeSync for AcpiScheme<'_, '_> { ["thermal"] => HandleKind::Thermal, ["power"] => HandleKind::Power, ["dmi"] => HandleKind::Dmi, + ["processor"] => HandleKind::Processor, ["tables", table] => { let signature = parse_table(table.as_bytes()).ok_or(Error::new(ENOENT))?; @@ -283,6 +305,19 @@ impl SchemeSync for AcpiScheme<'_, '_> { } } + ["processor", cpu_str, file] => { + // /scheme/acpi/processor//{pss,psd,cst,cpc} + let cpu: u32 = cpu_str.parse().map_err(|_| Error::new(EINVAL))?; + let kind = match *file { + "pss" => ProcFileKind::Pss, + "psd" => ProcFileKind::Psd, + "cst" => ProcFileKind::Cst, + "cpc" => ProcFileKind::Cpc, + _ => return Err(Error::new(ENOENT)), + }; + HandleKind::ProcFile { cpu, kind } + } + _ => return Err(Error::new(ENOENT)), } } @@ -366,6 +401,7 @@ impl SchemeSync for AcpiScheme<'_, '_> { // Build an owned buffer for DMI handles so the borrow does not // escape the match arm scope. let dmi_buf; + let proc_buf; let src_buf: &[u8] = match &handle.kind { HandleKind::Table(ref signature) => self .ctx @@ -386,7 +422,18 @@ impl SchemeSync for AcpiScheme<'_, '_> { .unwrap_or_default(); dmi_buf.as_bytes() } - _ => return Err(Error::new(EINVAL)), + HandleKind::Processor | HandleKind::DmiDir | HandleKind::Thermal | HandleKind::Power | HandleKind::Symbols(_) | HandleKind::RegisterPci | HandleKind::TopLevel | HandleKind::SchemeRoot => { + return Err(Error::new(EISDIR)); + } + HandleKind::ProcFile { .. } => { + // Per-CPU _PSS / _PSD / _CST / _CPC text export. The + // full AML→text conversion is a Phase G follow-up; for + // now, return a placeholder line so consumers + // (cpufreqd, redbear-power) can detect the path is + // present and report "no data" without getting ENOENT. + proc_buf = b"# ACPI processor data not yet populated\n".to_vec(); + proc_buf.as_slice() + } }; let offset = std::cmp::min(src_buf.len(), offset); @@ -410,7 +457,7 @@ impl SchemeSync for AcpiScheme<'_, '_> { match &handle.kind { HandleKind::TopLevel => { const TOPLEVEL_ENTRIES: &[&str] = &[ - "tables", "symbols", "thermal", "power", "dmi", + "tables", "symbols", "thermal", "power", "dmi", "processor", ]; for (idx, name) in TOPLEVEL_ENTRIES @@ -485,6 +532,21 @@ impl SchemeSync for AcpiScheme<'_, '_> { })?; } } + HandleKind::Processor => { + // Enumerate \_PR. entries from the AML namespace. + // Returns Ok with no entries on systems with no + // processors (headless QEMU with no DSDT) so consumers + // see an empty-but-existing directory. + let cpus = self.ctx.cpu_names(); + for (idx, cpu_name) in cpus.iter().enumerate().skip(opaque_offset as usize) { + buf.entry(DirEntry { + inode: 0, + next_opaque_id: idx as u64 + 1, + name: cpu_name.as_str(), + kind: DirentKind::Directory, + })?; + } + } HandleKind::Power => { // Enumerate PowerResource entries. On real laptops these // are AC adapters and battery controllers; on desktops @@ -502,10 +564,10 @@ impl SchemeSync for AcpiScheme<'_, '_> { HandleKind::Dmi => { // Consumers should `read_to_string("/scheme/acpi/dmi")` // rather than iterating, but we still surface the field - // list so that `ls /scheme/acpi/dmi/` produces a useful - // diagnostic on a live system. We always list the same - // set of fields regardless of whether SMBIOS data is - // present — empty entries just produce empty reads. + list so that ls /scheme/acpi/dmi/ produces a useful + diagnostic on a live system. We always list the same + set of fields regardless of whether SMBIOS data is + present -- empty entries just produce empty reads. for (idx, field) in DMI_FIELDS .iter() .enumerate() @@ -514,11 +576,16 @@ impl SchemeSync for AcpiScheme<'_, '_> { buf.entry(DirEntry { inode: 0, next_opaque_id: idx as u64 + 1, - name: *field, + name: field, kind: DirentKind::Regular, })?; } } + HandleKind::ProcFile { .. } | HandleKind::DmiDir => { + // No children; reads/writes go through the + // HandleKind match in kread/kwriteoff. + } + } _ => return Err(Error::new(EIO)), }