Files
RedBear-OS/src/scheme/acpi.rs
T
vasilito 8d9f9e552f kernel: s2idle MWAIT wake signal (Phase I.5)
Phase I.5: complete the acpid <-> kernel s2idle wire. After
MWAIT returns from an interrupt (typically an SCI from
acpid), the kernel now:

1. Clears S2IDLE_REQUESTED (via s2idle_request_clear)
2. Sets KSTOP_FLAG and triggers EVENT_READ on the kstop
   handle (via s2idle_signal_wake)

This is the kernel-side analog of Linux 7.1
`acpi_s2idle_wake` in `drivers/acpi/sleep.c:758`. The
existing irq_trigger in generic_irq has already routed the
SCI to acpid's listener (which opened /scheme/irq/{sci}
earlier in the boot sequence), so the AML interpretation
is done by acpid asynchronously.

The s2idle flow now:
1. acpid: enter_s2idle() (\_TTS(0), \_PTS(0), \_SST(3))
2. acpid: write 's2idle\n' to /scheme/sys/kstop
   -> kernel sets S2IDLE_REQUESTED, returns
3. Kernel idle path: mwait_loop() at deepest C-state
4. SCI breaks MWAIT (any interrupt, not just SCI)
5. Kernel mwait_loop post-handler (this commit):
   - s2idle_request_clear()
   - s2idle_signal_wake() -> KSTOP_FLAG set, EVENT_READ
6. acpid main loop: wakes from kstop handle read
7. acpid: exit_s2idle() (\_SST(2), \_WAK(0), \_SST(1))

The KSTOP_FLAG set in step 5 also serves as a 'reason'
indicator — acpid's CheckShutdown verb (kcall 2) returns
the flag, so acpid can distinguish a kstop-shutdown event
from a kstop-s2idle-wake event by polling CheckShutdown
after waking.

Hardware-agnostic: the same flow works for any platform
with Modern Standby firmware (Dell, HP, Lenovo, LG Gram,
etc.). The s2idle is the universal mechanism for low-power
idle; only the wake source (SCI, GPIO, RTC, ...) varies
per OEM.
2026-07-01 07:10:28 +03:00

213 lines
6.7 KiB
Rust

use alloc::boxed::Box;
use core::sync::atomic::{AtomicBool, Ordering};
use crate::sync::ordered::{Mutex, L4};
use spin::Once;
use syscall::data::GlobalSchemes;
use crate::{
acpi::{RxsdtEnum, RXSDT_ENUM},
context::file::InternalFlags,
scheme::{SchemeExt, StrOrBytes},
sync::CleanLockToken,
};
use crate::syscall::{
error::{Error, Result, EACCES, EBADFD, EINVAL, ENOENT},
flag::{AcpiVerb, CallFlags, EventFlags},
usercopy::UserSliceRw,
};
use super::{CallerCtx, KernelScheme, OpenResult};
/// A scheme used to access the RSDT or XSDT, and listen for shutdown, which is needed for e.g. `acpid` to function.
pub struct AcpiScheme;
bitflags! {
#[derive(PartialEq)]
struct HandleBits: usize {
const CAN_READ_RXSDT = 1;
const CAN_REGISTER_KSTOP = 2;
// mutually exclusive with the other flags
const KSTOP_HANDLE = 4;
}
}
static RXSDT_DATA: Once<Box<[u8]>> = Once::new();
static KSTOP_FLAG: Mutex<L4, bool> = Mutex::new(false);
static EXISTS_KSTOP_HANDLE: AtomicBool = AtomicBool::new(false);
/// Phase I: s2idle (Modern Standby / S0ix) coordination flag.
/// Set by `s2idle_request_set` (called from the kstop handler
/// when acpid writes "s2idle" to /scheme/sys/kstop). Read by
/// the kernel's idle path which calls `mwait_loop()` while
/// the flag is set. Cleared by `s2idle_request_clear` when an
/// SCI breaks the MWAIT, signaling the idle path to stop
/// calling `mwait_loop()`.
///
/// Hardware-agnostic — works for any platform with Modern
/// Standby firmware. Mirrors Linux 7.1
/// `s2idle_state == S2IDLE_STATE_ENTER` in
/// `kernel/power/suspend.c:91`.
static S2IDLE_REQUESTED: AtomicBool = AtomicBool::new(false);
/// Set by the kstop handler when acpid requests s2idle entry.
/// Idempotent.
pub fn s2idle_request_set() {
S2IDLE_REQUESTED.store(true, Ordering::Release);
}
/// Clear by the interrupt handler when an SCI breaks the MWAIT,
/// or by the s2idle wake path. Idempotent.
pub fn s2idle_request_clear() {
S2IDLE_REQUESTED.store(false, Ordering::Release);
}
/// Read by the kernel's idle path. Returns true if acpid has
/// requested s2idle entry and the kernel has not yet broken
/// out of MWAIT.
pub fn s2idle_requested() -> bool {
S2IDLE_REQUESTED.load(Ordering::Acquire)
}
/// Phase I: signal acpid that s2idle MWAIT was broken by an
/// interrupt. Called from `mwait_loop` after MWAIT returns.
/// Triggers the kstop handle's EVENT_READ so acpid's main loop
/// wakes and runs the \_SST(2) → \_WAK(0) → \_SST(1) AML
/// sequence on resume.
///
/// Mirrors Linux 7.1 `acpi_s2idle_wake` in
/// `drivers/acpi/sleep.c:758` — the kernel clears
/// s2idle_state and signals the userspace ACPI driver.
pub fn s2idle_signal_wake() {
let mut token = CleanLockToken::new();
*KSTOP_FLAG.lock(token.token()) = true;
if EXISTS_KSTOP_HANDLE.load(Ordering::Relaxed) {
crate::event::trigger(
GlobalSchemes::Acpi.scheme_id(),
HandleBits::KSTOP_HANDLE.bits(),
EventFlags::EVENT_READ,
&mut token,
);
}
}
pub fn register_kstop(token: &mut CleanLockToken) -> bool {
*KSTOP_FLAG.lock(token.token()) = true;
if !EXISTS_KSTOP_HANDLE.load(Ordering::Relaxed) {
error!("No userspace ACPI handler was notified when trying to shutdown. This is bad.");
// Let the kernel shutdown without ACPI.
return false;
}
crate::event::trigger(
GlobalSchemes::Acpi.scheme_id(),
HandleBits::KSTOP_HANDLE.bits(),
EventFlags::EVENT_READ,
token,
);
// TODO: Context switch directly to the waiting context, to avoid annoying timeouts.
true
}
impl AcpiScheme {
pub fn init() {
// NOTE: This __must__ be called from the main kernel context, while initializing all
// schemes. If it is called by any other context, then all ACPI data will probably not even
// be mapped.
let mut data_init = false;
RXSDT_DATA.call_once(|| {
data_init = true;
let table = match RXSDT_ENUM.get() {
Some(RxsdtEnum::Rsdt(rsdt)) => rsdt.as_slice(),
Some(RxsdtEnum::Xsdt(xsdt)) => xsdt.as_slice(),
None => {
warn!("expected RXSDT_ENUM to be initialized before AcpiScheme, is ACPI available?");
&[]
}
};
Box::from(table)
});
if !data_init {
error!("AcpiScheme::init called multiple times");
}
}
}
impl KernelScheme for AcpiScheme {
fn scheme_root(&self, _token: &mut CleanLockToken) -> Result<usize> {
Ok((HandleBits::CAN_READ_RXSDT | HandleBits::CAN_REGISTER_KSTOP).bits())
}
fn kopenat(
&self,
id: usize,
path: StrOrBytes,
_flags: usize,
_fcntl_flags: u32,
caller: CallerCtx,
_token: &mut CleanLockToken,
) -> Result<OpenResult> {
let bits = HandleBits::from_bits_retain(id);
let new_bits = match path.as_bytes() {
b"" | b"/" => bits,
b"kstop" | b"/kstop" => {
// TODO: can the uid check be removed?
if caller.uid != 0 || !bits.contains(HandleBits::CAN_REGISTER_KSTOP) {
return Err(Error::new(EACCES));
}
EXISTS_KSTOP_HANDLE.store(true, Ordering::Relaxed);
HandleBits::KSTOP_HANDLE
}
_ => return Err(Error::new(ENOENT)),
};
Ok(OpenResult::SchemeLocal(
new_bits.bits(),
InternalFlags::empty(),
))
}
fn kcall(
&self,
fds: &[usize],
payload: UserSliceRw,
flags: CallFlags,
metadata: &[u64],
token: &mut CleanLockToken,
) -> Result<usize> {
let [handle] = <&[usize; 1]>::try_from(fds)
.map_err(|_| Error::new(EINVAL))?
.map(HandleBits::from_bits_retain);
let verb = metadata
.get(0)
.copied()
.and_then(AcpiVerb::try_from_raw)
.ok_or(Error::new(EINVAL))?;
match verb {
AcpiVerb::ReadRxsdt => {
if !handle.contains(HandleBits::CAN_READ_RXSDT) || !flags.contains(CallFlags::READ)
{
return Err(Error::new(EINVAL));
}
let src = RXSDT_DATA.get().ok_or(Error::new(EBADFD))?;
payload.copy_common_bytes_from_slice(src)?;
Ok(src.len())
}
AcpiVerb::CheckShutdown => {
if handle != HandleBits::KSTOP_HANDLE {
return Err(Error::new(EINVAL));
}
Ok(usize::from(*KSTOP_FLAG.lock(token.token())))
}
}
}
}