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).
This commit is contained in:
Red Bear OS
2026-06-30 06:32:09 +03:00
parent d844111937
commit 8140a2cd27
2 changed files with 197 additions and 64 deletions
+161 -50
View File
@@ -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<String> {
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<String> {
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::<u16>::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::<u16>::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::<u16>::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<u64, AmlEvalError> {
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)]
+36 -14
View File
@@ -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.<zone> 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")`