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.
This commit is contained in:
Red Bear OS
2026-07-22 12:22:36 +09:00
parent cf2abc64b0
commit a12fb9fc7c
3 changed files with 323 additions and 0 deletions
+4
View File
@@ -1841,6 +1841,10 @@ impl Fadt {
),
}
}
pub fn supports_s0_idle(&self) -> bool {
(self.flags & (1 << 21)) != 0
}
}
impl Deref for Fadt {
+30
View File
@@ -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");
+289
View File
@@ -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<LpiEntry>,
}
impl LpitInfo {
pub fn parse(data: &[u8]) -> Option<Self> {
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<u32>,
pub gpe_device_path: Option<String>,
pub sleep_state: u8,
}
pub struct WakeRegistry {
devices: RwLock<Vec<WakeDevice>>,
}
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<WakeDevice> {
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<u32> {
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<LpitInfo> {
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);
}
}