From a12fb9fc7c4da779cc62b4e30ce5b88d9c440196 Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Wed, 22 Jul 2026 12:22:36 +0900 Subject: [PATCH] acpid: LPIT parser + _PRW wake enumeration + FADT S0-idle detection (ACPICA port) Port useful ACPICA components into the existing Rust ACPI stack: - LPIT (Low Power Idle Table) parser in new acpid/wake.rs. Parses native C-state LPI entries (entry trigger, residency, latency, residency counter, counter frequency). Computes total residency and deepest LPI state. 4 unit tests cover parsing, empty input, short input, and deepest-entry selection. Reference: Linux include/acpi/actbl2.h struct acpi_lpit_native. - _PRW wake device enumeration in acpid/wake.rs. WakeRegistry::enumerate() evaluates _PRW on known wake-capable device paths (LID, PWRB, SLPB, XHCI, HDAS, CNVW, I2C0/1, THC0/1). Extracts GPE number and sleep state from the _PRW package. Reference: Linux drivers/acpi/scan.c acpi_bus_get_wakeup_device_flags. - FADT S0-idle detection: Fadt::supports_s0_idle() checks bit 21 (ACPI_FADT_LOW_POWER_S0) to detect platforms that support s2idle. Reference: Linux include/acpi/actbl.h. - acpid/main.rs: LPIT parse + wake enumeration + S0-idle detection wired into init after AcpiContext::init() and before power events. ACPICA assessment: do NOT port ACPICA as a C library. Port its useful data structures and algorithms into the existing Rust stack (acpi-rs vendored fork + acpid daemon). The AML interpreter already has comprehensive opcode coverage (only DefLoad/DefLoadTable remain unimplemented, both optional). The GPE/EC/fixed-event infrastructure is already in acpid. LPIT + _PRW + S0-idle detection fill the remaining gaps for Phase 9.1 s2idle completion. --- drivers/acpid/src/acpi.rs | 4 + drivers/acpid/src/main.rs | 30 ++++ drivers/acpid/src/wake.rs | 289 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 323 insertions(+) create mode 100644 drivers/acpid/src/wake.rs diff --git a/drivers/acpid/src/acpi.rs b/drivers/acpid/src/acpi.rs index 6209fc5b60..38124fd9a5 100644 --- a/drivers/acpid/src/acpi.rs +++ b/drivers/acpid/src/acpi.rs @@ -1841,6 +1841,10 @@ impl Fadt { ), } } + + pub fn supports_s0_idle(&self) -> bool { + (self.flags & (1 << 21)) != 0 + } } impl Deref for Fadt { diff --git a/drivers/acpid/src/main.rs b/drivers/acpid/src/main.rs index 260eb0c09a..032b50b159 100644 --- a/drivers/acpid/src/main.rs +++ b/drivers/acpid/src/main.rs @@ -18,6 +18,7 @@ mod ec; mod gpe; mod notifications; mod power_events; +mod wake; mod scheme; @@ -87,6 +88,35 @@ fn daemon(daemon: daemon::Daemon) -> ! { ]; let acpi_context = self::acpi::AcpiContext::init(physaddrs_iter, region_handlers); + // LPIT parse for s2idle residency reporting (Phase 9.1). + if let Some(lpit) = wake::init_lpit(&acpi_context) { + log::info!( + "acpid: LPIT loaded — {} LPI state(s), deepest {} us residency", + lpit.entries.len(), + lpit.deepest_lpi().map(|e| e.residency_us).unwrap_or(0) + ); + } else { + log::debug!("acpid: no LPIT table found (s2idle residency reporting unavailable)"); + } + + if let Some(fadt) = acpi_context.fadt() { + if fadt.supports_s0_idle() { + log::info!("acpid: platform supports S0 low-power idle (s2idle)"); + } else { + log::info!("acpid: platform does not advertise S0 low-power idle — s2idle unavailable"); + } + } + + // _PRW wake device enumeration for s2idle wake management. + let wake_registry = wake::WakeRegistry::enumerate(&acpi_context); + if wake_registry.device_count() > 0 { + log::info!( + "acpid: {} wake-capable device(s) registered (GPEs: {:?})", + wake_registry.device_count(), + wake_registry.wake_gpes() + ); + } + // TODO: I/O permission bitmap? #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] common::acquire_port_io_rights().expect("acpid: failed to set I/O privilege level to Ring 3"); diff --git a/drivers/acpid/src/wake.rs b/drivers/acpid/src/wake.rs new file mode 100644 index 0000000000..b302f5be71 --- /dev/null +++ b/drivers/acpid/src/wake.rs @@ -0,0 +1,289 @@ +use std::sync::RwLock; + +use log::{debug, info, warn}; + +use crate::acpi::AcpiContext; + +const LPIT_TYPE_NATIVE_CSTATE: u32 = 0x00; + +#[derive(Clone, Debug)] +pub struct LpiEntry { + pub unique_id: u16, + pub entry_trigger_addr: u64, + pub residency_us: u32, + pub latency_us: u32, + pub residency_counter_addr: u64, + pub counter_frequency: u64, + pub disabled: bool, + pub no_counter: bool, +} + +#[derive(Clone, Debug, Default)] +pub struct LpitInfo { + pub entries: Vec, +} + +impl LpitInfo { + pub fn parse(data: &[u8]) -> Option { + if data.len() < 8 { + return None; + } + let mut entries = Vec::new(); + let mut offset = 0; + while offset + 16 <= data.len() { + let type_ = u32::from_le_bytes([ + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + ]); + let length = u32::from_le_bytes([ + data[offset + 4], + data[offset + 5], + data[offset + 6], + data[offset + 7], + ]) as usize; + if length < 16 || offset + length > data.len() { + break; + } + let unique_id = u16::from_le_bytes([data[offset + 8], data[offset + 9]]); + let flags = u32::from_le_bytes([ + data[offset + 12], + data[offset + 13], + data[offset + 14], + data[offset + 15], + ]); + let disabled = (flags & 1) != 0; + let no_counter = (flags & 2) != 0; + + if type_ == LPIT_TYPE_NATIVE_CSTATE && length >= 48 { + let entry_trigger_addr = u64::from_le_bytes([ + data[offset + 16], + data[offset + 17], + data[offset + 18], + data[offset + 19], + data[offset + 20], + data[offset + 21], + data[offset + 22], + data[offset + 23], + ]); + let residency_us = u32::from_le_bytes([ + data[offset + 24], + data[offset + 25], + data[offset + 26], + data[offset + 27], + ]); + let latency_us = u32::from_le_bytes([ + data[offset + 28], + data[offset + 29], + data[offset + 30], + data[offset + 31], + ]); + let residency_counter_addr = u64::from_le_bytes([ + data[offset + 32], + data[offset + 33], + data[offset + 34], + data[offset + 35], + data[offset + 36], + data[offset + 37], + data[offset + 38], + data[offset + 39], + ]); + let counter_frequency = u64::from_le_bytes([ + data[offset + 40], + data[offset + 41], + data[offset + 42], + data[offset + 43], + data[offset + 44], + data[offset + 45], + data[offset + 46], + data[offset + 47], + ]); + entries.push(LpiEntry { + unique_id, + entry_trigger_addr, + residency_us, + latency_us, + residency_counter_addr, + counter_frequency, + disabled, + no_counter, + }); + } + offset += length; + } + if entries.is_empty() { + None + } else { + Some(Self { entries }) + } + } + + pub fn total_residency_us(&self) -> u64 { + self.entries + .iter() + .filter(|e| !e.disabled) + .map(|e| u64::from(e.residency_us)) + .sum() + } + + pub fn deepest_lpi(&self) -> Option<&LpiEntry> { + self.entries + .iter() + .filter(|e| !e.disabled) + .max_by_key(|e| e.residency_us) + } +} + +#[derive(Clone, Debug)] +pub struct WakeDevice { + pub acpi_path: String, + pub gpe_number: Option, + pub gpe_device_path: Option, + pub sleep_state: u8, +} + +pub struct WakeRegistry { + devices: RwLock>, +} + +impl WakeRegistry { + pub fn new() -> Self { + Self { + devices: RwLock::new(Vec::new()), + } + } + + pub fn enumerate(acpi: &AcpiContext) -> Self { + let registry = Self::new(); + let candidates = [ + "\\_SB.LID_", + "\\_SB.PWRB", + "\\_SB.SLPB", + "\\_SB.PC00.XHCI", + "\\_SB.PC00.HDAS", + "\\_SB.PC00.CNVW", + "\\_SB.PC00.I2C0", + "\\_SB.PC00.I2C1", + "\\_SB.PC00.THC0", + "\\_SB.PC00.THC1", + ]; + for path in &candidates { + if let Some(wake) = registry.evaluate_prw(acpi, path) { + info!( + "acpid: wake device {} (GPE {:?}, S{})", + wake.acpi_path, wake.gpe_number, wake.sleep_state + ); + registry.devices.write().unwrap().push(wake); + } + } + let count = registry.devices.read().unwrap().len(); + if count > 0 { + info!("acpid: {} wake-capable device(s) enumerated", count); + } else { + debug!("acpid: no wake-capable devices found via _PRW"); + } + registry + } + + fn evaluate_prw(&self, acpi: &AcpiContext, path: &str) -> Option { + let values = acpi.evaluate_acpi_method(path, "_PRW", &[]).ok()?; + if values.len() < 2 { + return None; + } + + let gpe_number = Some(values[0] as u32); + let sleep_state = values[1] as u8; + + Some(WakeDevice { + acpi_path: path.to_string(), + gpe_number, + gpe_device_path: None, + sleep_state, + }) + } + + pub fn wake_gpes(&self) -> Vec { + self.devices + .read() + .unwrap() + .iter() + .filter_map(|d| d.gpe_number) + .collect() + } + + pub fn device_count(&self) -> usize { + self.devices.read().unwrap().len() + } +} + +pub fn init_lpit(acpi: &AcpiContext) -> Option { + let lpit_sdt = acpi.take_single_sdt(*b"LPIT")?; + let info = LpitInfo::parse(lpit_sdt.data())?; + info!( + "acpid: LPIT parsed — {} LPI entries, total residency {} us, deepest {} us", + info.entries.len(), + info.total_residency_us(), + info.deepest_lpi().map(|e| e.residency_us).unwrap_or(0) + ); + Some(info) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lpit_parse_native_cstate() { + let mut data = Vec::new(); + data.extend_from_slice(&0u32.to_le_bytes()); + data.extend_from_slice(&48u32.to_le_bytes()); + data.extend_from_slice(&1u16.to_le_bytes()); + data.extend_from_slice(&0u16.to_le_bytes()); + data.extend_from_slice(&0u32.to_le_bytes()); + data.extend_from_slice(&0x1000u64.to_le_bytes()); + data.extend_from_slice(&300u32.to_le_bytes()); + data.extend_from_slice(&10u32.to_le_bytes()); + data.extend_from_slice(&0x2000u64.to_le_bytes()); + data.extend_from_slice(&19200u64.to_le_bytes()); + let info = LpitInfo::parse(&data).unwrap(); + assert_eq!(info.entries.len(), 1); + assert_eq!(info.entries[0].unique_id, 1); + assert_eq!(info.entries[0].residency_us, 300); + assert_eq!(info.entries[0].latency_us, 10); + assert!(!info.entries[0].disabled); + assert!(!info.entries[0].no_counter); + } + + #[test] + fn lpit_parse_empty() { + assert!(LpitInfo::parse(&[]).is_none()); + } + + #[test] + fn lpit_parse_too_short() { + let data = [0u8; 7]; + assert!(LpitInfo::parse(&data).is_none()); + } + + #[test] + fn lpit_deepest_entry() { + let mut data = Vec::new(); + for (id, residency) in [(1u16, 100u32), (2, 500), (3, 50)] { + data.extend_from_slice(&0u32.to_le_bytes()); + data.extend_from_slice(&48u32.to_le_bytes()); + data.extend_from_slice(&id.to_le_bytes()); + data.extend_from_slice(&0u16.to_le_bytes()); + data.extend_from_slice(&0u32.to_le_bytes()); + data.extend_from_slice(&0x1000u64.to_le_bytes()); + data.extend_from_slice(&residency.to_le_bytes()); + data.extend_from_slice(&10u32.to_le_bytes()); + data.extend_from_slice(&0x2000u64.to_le_bytes()); + data.extend_from_slice(&19200u64.to_le_bytes()); + } + let info = LpitInfo::parse(&data).unwrap(); + let deepest = info.deepest_lpi().unwrap(); + assert_eq!(deepest.unique_id, 2); + assert_eq!(deepest.residency_us, 500); + } +}