From 8140a2cd2753d8b85d536068bf4a79b8a5a7e961 Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Tue, 30 Jun 2026 06:32:09 +0300 Subject: [PATCH] base: refactor set_global_s_state to follow Linux 7.1 acpi_enter_sleep_state Phase D of the ACPI fork-sync plan. Refactors acpi.rs set_global_s_state to follow the canonical Linux 7.1 pattern from drivers/acpi/acpica/hwxfsleep.c:283 (acpi_enter_sleep_state): 1. Look up the _Sx package in the AML namespace, extract SLP_TYPa and SLP_TYPb (was previously hardcoded to _S5). 2. Evaluate _PTS(state) AML method (Prepare To Sleep) via the new aml_evaluate_simple_method helper. Failure is non-fatal: _PTS is optional per ACPI spec. 3. Evaluate _SST(sst_value) AML method (System Status indicator) with the ACPI_SST_* constants (working=0, sleeping=1, sleep-context=2, indicator-off=7). 4. Write SLP_EN|SLP_TYPa to PM1a, SLP_EN|SLP_TYPb to PM1b. 5. Spin (machine should power off before this returns). Also adds: - Generic aml_evaluate_simple_method(path, arg) helper that mirrors Linux 7.1 acpi_execute_simple_method (drivers/acpi/utils.c). Uses evaluate_if_present so missing methods return Ok(None) cleanly instead of AmlError::ObjectDoesNotExist. Takes the AML global lock with timeout 16 (mirroring the existing aml_eval pattern). - Removes the hardcoded `if state != 5` early-return; the function now handles any S-state generically. S1-S4 paths still don't fully work (no _WAK, no P-state preservation, no wakeup vector), but the new generic structure means a future _WAK implementation only needs to add wakeup handling after step 4. - Keeps the existing SLP_TYPb write (from Phase C) for hardware that requires both PM1a and PM1b writes. Combined with the existing scheme.rs change (thermal_zones() and power_adapters() methods that enumerate _TZ and PowerResource entries from the AML namespace), this completes the major ACPI subsystem gaps identified by the 2026-06-30 assessment: - Gap #1 RSDP validation (closed in Phase A) - Gap #3 AML mutex stubs (closed in Phase C) - Gap #4 set_global_s_state genericity + _PTS + _SST (closed here) - Gap #5 SLP_TYPb write (closed in Phase C) - Gap #6 parse_lnk_irc range validation (closed in Phase C) - Gap #7 thermal/power enumeration (closed in Phase C) - Gap #8 AcpiScheme fevent (closed in Phase A) Remaining open: - Gap #2 DMAR init (needs real-hardware investigation) - Gap #4b _WAK infrastructure for real S1-S4 suspend (the generic Sx scaffolding is now in place; _WAK + wakeup vector + P-state preservation are still TBD) 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 06:28. QEMU boot reaches Red Bear login: prompt cleanly with redbear-sessiond working (login1 registered on D-Bus, ACPI shutdown watcher no longer errors). --- drivers/acpid/src/acpi.rs | 211 +++++++++++++++++++++++++++--------- drivers/acpid/src/scheme.rs | 50 ++++++--- 2 files changed, 197 insertions(+), 64 deletions(-) diff --git a/drivers/acpid/src/acpi.rs b/drivers/acpid/src/acpi.rs index 5ab7ef155e..92226c4273 100644 --- a/drivers/acpid/src/acpi.rs +++ b/drivers/acpid/src/acpi.rs @@ -572,6 +572,49 @@ impl AcpiContext { } } + /// Enumerate thermal zones under `\_TZ` by walking the namespace. + /// Returns zone names (the children of `\_TZ` whose serialized form + /// is a `ThermalZone` object). An empty Vec means the system has no + /// thermal zones (typical for headless QEMU and desktops). + pub fn thermal_zones(&self) -> Vec { + let mut zones = Vec::new(); + let Ok(aml_symbols) = self.aml_symbols(None) else { + return zones; + }; + let prefix = "\\_TZ."; + for name in aml_symbols.symbols_cache().keys() { + // Match immediate children of \_TZ, not deeper paths. + // E.g. "\_TZ.TZ0" matches, "\_TZ.TZ0._TMP" doesn't. + if let Some(rest) = name.strip_prefix(prefix) { + if !rest.contains('.') { + zones.push(name.clone()); + } + } + } + zones + } + + /// Enumerate AC adapter objects. Returns adapter names whose + /// serialized form is a `PowerResource`. AC adapter detection on + /// laptops requires EC queries that are not yet wired up; this + /// function returns whatever the AML namespace has declared. + pub fn power_adapters(&self) -> Vec { + let mut adapters = Vec::new(); + let Ok(aml_symbols) = self.aml_symbols(None) else { + return adapters; + }; + // AC adapters typically live under \_SB.AC or \_SB.PCI0.AC; + // we surface anything whose serialized form is a PowerResource. + // This is a best-effort enumeration until we have EC-based + // battery + AC adapter detection. + for (name, value) in aml_symbols.symbols_cache().iter() { + if value.contains("PowerResource(") { + adapters.push(name.clone()); + } + } + adapters + } + pub fn aml_symbols( &self, pci_fd: Option<&libredox::Fd>, @@ -605,17 +648,15 @@ impl AcpiContext { /// See https://uefi.org/sites/default/files/resources/ACPI_6_1.pdf /// - search for PM1a /// See https://forum.osdev.org/viewtopic.php?t=16990 for practical details + /// + /// Follows the Linux 7.1 `acpi_enter_sleep_state` pattern + /// (drivers/acpi/acpica/hwxfsleep.c:283): + /// 1. Look up `_Sx` package in AML namespace, extract SLP_TYPa/SLP_TYPb + /// 2. Evaluate `_PTS(state)` (Prepare To Sleep) - optional + /// 3. Evaluate `_SST(sst_value)` (System Status) - optional + /// 4. Write SLP_EN|SLP_TYPa to PM1a, SLP_EN|SLP_TYPb to PM1b + /// 5. Spin (machine should power off before this returns) pub fn set_global_s_state(&self, state: u8) { - if state != 5 { - // Only S5 (soft-off) is implemented. S1-S4 (suspend/hibernate) - // would require _PTS/_WAK AML method evaluation, P-state - // preservation, and a wakeup path - not yet wired up. - log::warn!( - "set_global_s_state({}) called but only S5 is implemented; ignoring", - state - ); - return; - } let fadt = match self.fadt() { Some(fadt) => fadt, None => { @@ -624,24 +665,30 @@ impl AcpiContext { } }; - let port = fadt.pm1a_control_block as u16; - let mut val = 1 << 13; - let aml_symbols = self.aml_symbols.read(); - let s5_aml_name = match acpi::aml::namespace::AmlName::from_str("\\_S5") { + // Step 1: Look up the `_Sx` package for this state and extract + // SLP_TYPa/SLP_TYPb. Mirrors acpi_get_sleep_type_data() in Linux. + let sx_name_str = format!("\\_S{}", state); + let sx_aml_name = match acpi::aml::namespace::AmlName::from_str(&sx_name_str) { Ok(aml_name) => aml_name, Err(error) => { - log::error!("Could not build AmlName for \\_S5, {:?}", error); + log::error!("Could not build AmlName for \\_S{}, {:?}", state, error); return; } }; - let s5 = match &aml_symbols.aml_context { - Some(aml_context) => match aml_context.namespace.lock().get(s5_aml_name) { - Ok(s5) => s5, + let package = match &aml_symbols.aml_context { + Some(aml_context) => match aml_context.namespace.lock().get(sx_aml_name) { + Ok(obj) => match obj.deref() { + acpi::aml::object::Object::Package(package) => package.clone(), + _ => { + log::error!("Cannot set S-state, \\_S{} is not a package", state); + return; + } + }, Err(error) => { - log::error!("Cannot set S-state, missing \\_S5, {:?}", error); + log::error!("Cannot set S-state, missing \\_S{}, {:?}", state, error); return; } }, @@ -651,45 +698,75 @@ impl AcpiContext { } }; - let package = match s5.deref() { - acpi::aml::object::Object::Package(package) => package, - _ => { - log::error!("Cannot set S-state, \\_S5 is not a package"); + let slp_typa = match package.get(0).and_then(|e| match e.deref() { + acpi::aml::object::Object::Integer(i) => Some(i.to_owned()), + _ => None, + }) { + Some(v) => v, + None => { + log::error!("\\_S{} SLP_TYPa is not an Integer", state); + return; + } + }; + let slp_typb = match package.get(1).and_then(|e| match e.deref() { + acpi::aml::object::Object::Integer(i) => Some(i.to_owned()), + _ => None, + }) { + Some(v) => v, + None => { + log::error!("\\_S{} SLP_TYPb is not an Integer", state); return; } }; - let slp_typa = match package[0].deref() { - acpi::aml::object::Object::Integer(i) => i.to_owned(), - _ => { - log::error!("typa is not an Integer"); - return; - } - }; - let slp_typb = match package[1].deref() { - acpi::aml::object::Object::Integer(i) => i.to_owned(), - _ => { - log::error!("typb is not an Integer"); - return; - } - }; + log::trace!( + "Sleep state S{} SLP_TYPa {:X}, SLP_TYPb {:X}", + state, slp_typa, slp_typb + ); - log::trace!("Shutdown SLP_TYPa {:X}, SLP_TYPb {:X}", slp_typa, slp_typb); - val |= slp_typa as u16; + // Step 2: Evaluate `_PTS(state)` (Prepare To Sleep). + // Mirrors Linux 7.1 drivers/acpi/acpica/hwxfsleep.c:222-233. + // ACPI spec says `_PTS` is optional; AE_NOT_FOUND is not fatal. + if let Err(e) = self.aml_evaluate_simple_method("\\_PTS", state as u64) { + log::debug!("\\_PTS({}) not evaluated ({}), continuing", state, e); + } + + // Step 3: Evaluate `_SST(sst_value)` (System Status indicator). + // Mirrors Linux 7.1 ACPI_SST_* constants: + // working=0, sleeping=1, sleep-context=2, indicator-off=7. + let sst_value: u64 = match state { + 0 => 0, + 1..=3 => 1, + 4 => 2, + _ => 7, + }; + if let Err(e) = self.aml_evaluate_simple_method("\\_SST", sst_value) { + log::debug!("\\_SST({}) not evaluated ({}), continuing", sst_value, e); + } + + // Step 4: Write SLP_EN | SLP_TYPa to PM1a, SLP_EN | SLP_TYPb to PM1b. + let slp_en: u16 = 1 << 13; + let val_a = slp_en | slp_typa as u16; + let val_b = slp_en | slp_typb as u16; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] { - log::warn!("Shutdown with ACPI outw(0x{:X}, 0x{:X})", port, val); - Pio::::new(port).write(val); + let port_a = fadt.pm1a_control_block as u16; + log::warn!( + "Sleep S{} with ACPI outw(0x{:X}, 0x{:X})", + state, port_a, val_a + ); + Pio::::new(port_a).write(val_a); - // Some hardware requires both PM1a and PM1b to be written for S5. - // The FADT pm1b_control_block is 0 when no second block exists; - // in that case skip the second write. + // Some hardware requires both PM1a and PM1b to be written for + // the sleep transition. The FADT pm1b_control_block is 0 when + // no second block exists; in that case skip the second write. let port_b = fadt.pm1b_control_block as u16; if port_b != 0 { - let mut val_b = 1 << 13; - val_b |= slp_typb as u16; - log::warn!("Shutdown with ACPI outw(0x{:X}, 0x{:X})", port_b, val_b); + log::warn!( + "Sleep S{} with ACPI outw(0x{:X}, 0x{:X})", + state, port_b, val_b + ); Pio::::new(port_b).write(val_b); } } @@ -697,16 +774,50 @@ impl AcpiContext { #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))] { log::error!( - "Cannot shutdown with ACPI outw(0x{:X}, 0x{:X}) on this architecture", - port, - val + "Cannot sleep with ACPI on this architecture: state=S{} val_a={:X} val_b={:X}", + state, val_a, val_b ); } + // Step 5: Spin. On a working ACPI implementation, the machine + // powers off (or suspends) before this returns. loop { core::hint::spin_loop(); } } + + /// Evaluate a simple AML method that takes one integer argument and + /// returns an integer (or nothing). Mirrors Linux 7.1 + /// `acpi_execute_simple_method` (drivers/acpi/utils.c). + /// + /// Returns `Ok(0)` if the method is not present (the ACPI spec + /// allows `_PTS`/`_SST`/etc. to be optional), `Ok(integer)` if + /// it returned an integer, or `Err` on evaluation failure. + fn aml_evaluate_simple_method(&self, path: &str, arg: u64) -> Result { + let mut symbols = self.aml_symbols.write(); + let aml_name = acpi::aml::namespace::AmlName::from_str(path) + .map_err(|_| AmlEvalError::DeserializationError)?; + let interpreter = symbols.aml_context_mut(None)?; + interpreter.acquire_global_lock(16)?; + + let args = vec![acpi::aml::object::Object::Integer(arg).wrap()]; + + // Use evaluate_if_present so missing methods return Ok(None) cleanly + // instead of AmlError::ObjectDoesNotExist. + let result = interpreter.evaluate_if_present(aml_name, args); + interpreter + .release_global_lock() + .expect("acpid: failed to release AML global lock"); + + match result { + Ok(Some(obj)) => match obj.deref().as_integer() { + Ok(n) => Ok(n), + Err(_) => Ok(0), + }, + Ok(None) => Ok(0), + Err(e) => Err(AmlEvalError::from(e)), + } + } } #[repr(C, packed)] diff --git a/drivers/acpid/src/scheme.rs b/drivers/acpid/src/scheme.rs index 43e0229b43..5e2320474e 100644 --- a/drivers/acpid/src/scheme.rs +++ b/drivers/acpid/src/scheme.rs @@ -44,15 +44,15 @@ enum HandleKind<'a> { Symbol { name: String, description: String }, SchemeRoot, RegisterPci, - /// `/scheme/acpi/thermal` — always present, currently empty. The - /// ACPI `_TZ` namespace iteration that would populate it has not - /// been wired into this fork yet; thermald and `redbear-info` both - /// treat an empty directory as "no zones, serve empty surface". + /// `/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` — same story: UPower (and friends) probe - /// `power/adapters/` and `power/batteries/`. Empty directory means - /// "no ACPI-listed power sources on this machine", which is the - /// correct fallback for desktops/headless QEMU. + /// `/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 /// fields (consumed by `redox-driver-sys` quirks loader). @@ -470,12 +470,34 @@ impl SchemeSync for AcpiScheme<'_, '_> { })?; } } - HandleKind::Thermal | HandleKind::Power => { - // Empty placeholder directories. Consumers (thermald, - // redbear-upower) iterate them with read_dir and gracefully - // handle the empty result. Returning Ok with no entries - // is what `read_dir` expects for an existing-but-empty - // directory. + HandleKind::Thermal => { + // Enumerate \_TZ. entries from the AML namespace. + // Returns Ok with no entries on systems with no zones + // (headless QEMU, desktops) so consumers see an + // empty-but-existing directory. + let zones = self.ctx.thermal_zones(); + for (idx, zone) in zones.iter().enumerate().skip(opaque_offset as usize) { + buf.entry(DirEntry { + inode: 0, + next_opaque_id: idx as u64 + 1, + name: zone.as_str(), + kind: DirentKind::Directory, + })?; + } + } + HandleKind::Power => { + // Enumerate PowerResource entries. On real laptops these + // are AC adapters and battery controllers; on desktops + // and QEMU the list is empty. + let adapters = self.ctx.power_adapters(); + for (idx, adapter) in adapters.iter().enumerate().skip(opaque_offset as usize) { + buf.entry(DirEntry { + inode: 0, + next_opaque_id: idx as u64 + 1, + name: adapter.as_str(), + kind: DirentKind::Directory, + })?; + } } HandleKind::Dmi => { // Consumers should `read_to_string("/scheme/acpi/dmi")`