acpid+ps2d: wire SystemQuirkFlags consumers (LG Gram Round 1)

acpid: load SystemQuirkFlags from in-memory DMI at init via
redox-driver-sys::quirks::toml_loader::load_dmi_system_quirks.
Store as a field on AcpiContext and expose via system_quirks().
Wire two consumers:

  - FORCE_S2IDLE in set_global_s_state(3): routes S3 entry to
    s2idle preparation instead. LG Gram 16Z90TP, Framework 16,
    late Dell XPS advertise \_S3 in DSDT but the firmware refuses
    the SLP_TYP write and hangs / silently no-ops.

  - NO_LEGACY_PM1B in set_global_s_state(*): skips the PM1b write
    even when FADT reports a non-zero pm1b_control_block. LG
    firmware reports a PM1b block that does not exist on the
    platform; writes wedge the controller.

ps2d: load SystemQuirkFlags from /scheme/acpi/dmi at init via
redox_driver_sys::quirks::system_quirks(). Wire one consumer:

  - KBD_DEACTIVATE_FIXUP in Ps2::init(): skips SetDefaultsDisable
    (0xF5) during keyboard init. LG Gram + some Dell/HP/Lenovo
    keyboards wedge or drop keys when 0xF5 is sent (Linux
    atkbd.c keyboard_broken[] / atkbd_deactivate_input tables).

acpid/src/dmi.rs: reframe misleading 'stub' docstring on
try_load_existing() — the function is a real round-trip reader
for /scheme/acpi/dmi, not a stub.

Cargo.lock regenerated to include the new redox-driver-sys path
dependency in acpid and ps2d.

This is the consumer-wiring deliverable for LG Gram Round 1
(local/docs/evidence/lg-gram/ASSESSMENT-2026-07-26.md). The
matching redox-driver-sys stub-replacement work
(load_dmi_acpi_quirks real loader, PANEL_ORIENTATION_TABLE
populated with Linux DRM entries, ACPI_FLAG_NAMES + TOML parser)
lands in the parent repo in the same Round 1 push.

acpi_irq1_skip_override remains documented as needing kernel IRQ
setup work (out of scope for Round 1).
This commit is contained in:
Red Bear OS
2026-07-26 16:30:50 +09:00
parent 9e5f915d42
commit 45452c5a8e
8 changed files with 143 additions and 13 deletions
Generated
+3
View File
@@ -2609,6 +2609,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "usb-core"
version = "0.3.1"
dependencies = [
"log",
]
[[package]]
name = "usbctl"
+7 -1
View File
@@ -23,11 +23,17 @@ ron.workspace = true
serde.workspace = true
amlserde = { path = "../amlserde" }
common = { path = "../common" }
common = { path = "../../common" }
daemon = { path = "../../daemon" }
libredox.workspace = true
redox-scheme.workspace = true
scheme-utils = { path = "../../scheme-utils" }
# Quirks system: acpid is the consumer of SystemQuirkFlags (force_s2idle,
# acpi_irq1_skip_override, no_legacy_pm1b). Loaded at init from the in-memory
# DMI scan via the toml_loader path. Mirrors the cross-tree dep pattern used
# by xhcid (drivers/usb/xhcid/Cargo.toml).
redox-driver-sys = { path = "../../../../recipes/drivers/redox-driver-sys/source" }
[lints]
workspace = true
+84 -4
View File
@@ -418,6 +418,17 @@ pub struct AcpiContext {
/// SMBIOS, and downstream quirks systems tolerate the absence.
dmi: Option<DmiInfo>,
/// Machine-wide system quirk flags accumulated from `[[dmi_system_quirk]]`
/// entries whose DMI rule matched this host. Computed once at init from
/// the in-memory DMI scan, then consumed by:
/// - [`Self::set_global_s_state`] (`NO_LEGACY_PM1B` — skip PM1b write)
/// - [`Self::enter_s2idle`] / `enter_s3` (`FORCE_S2IDLE` — route Modern
/// Standby platforms away from S3)
/// `acpi_irq1_skip_override` and `kbd_deactivate_fixup` are owned by
/// other daemons (kernel IRQ setup and ps2d respectively) and are read
/// by those consumers via `/scheme/acpi/dmi`.
system_quirks: redox_driver_sys::quirks::SystemQuirkFlags,
pub next_ctx: RwLock<u64>,
/// GPE/PM1 fixed-event register map, built from the FADT by
@@ -581,6 +592,7 @@ impl AcpiContext {
sdt_order: RwLock::new(Vec::new()),
dmi: None,
system_quirks: redox_driver_sys::quirks::SystemQuirkFlags::empty(),
facs: None,
};
@@ -623,6 +635,35 @@ impl AcpiContext {
}
}
// Compute machine-wide system quirk flags from the in-memory DMI
// scan. The flags drive consumer behavior in this daemon (and are
// also re-readable by other daemons via /scheme/acpi/dmi). The
// conversion maps the 7 fields redox_driver_sys::quirks::dmi::DmiInfo
// knows about; the remaining 9 fields in acpid::dmi::DmiInfo are
// not consumed by the quirk system today.
this.system_quirks = this.dmi.as_ref().map_or(
redox_driver_sys::quirks::SystemQuirkFlags::empty(),
|info| {
let sys_dmi = redox_driver_sys::quirks::dmi::DmiInfo {
sys_vendor: info.sys_vendor.clone(),
board_vendor: info.board_vendor.clone(),
board_name: info.board_name.clone(),
board_version: info.board_version.clone(),
product_name: info.product_name.clone(),
product_version: info.product_version.clone(),
bios_version: info.bios_version.clone(),
};
redox_driver_sys::quirks::toml_loader::load_dmi_system_quirks(&sys_dmi)
.unwrap_or_default()
},
);
if !this.system_quirks.is_empty() {
log::info!(
"acpid: loaded system quirks: {:?}",
this.system_quirks
);
}
Fadt::init(&mut this);
// DMAR init is opt-in via REDBEAR_DMAR_INIT=1 env var. MMIO reads
// (e.g. gl_sts.read()) on some real hardware block or spin
@@ -642,6 +683,14 @@ impl AcpiContext {
self.dmi.as_ref()
}
/// Machine-wide system quirk flags accumulated at init from
/// `[[dmi_system_quirk]]` TOML entries that matched this host's DMI.
/// Empty when no SMBIOS or no rules apply — consumers must treat that
/// as "no quirks".
pub fn system_quirks(&self) -> redox_driver_sys::quirks::SystemQuirkFlags {
self.system_quirks
}
/// Access the parsed FACS (Firmware ACPI Control Structure), if
/// present. Returns `None` if no FACS table was found in the RSDT/XSDT.
pub fn facs(&self) -> Option<&Facs> {
@@ -1021,6 +1070,25 @@ impl AcpiContext {
/// 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) {
// FORCE_S2IDLE system quirk: when set, route S3 entry to s2idle
// instead. Modern Standby-only platforms (LG Gram 16Z90TP, Framework
// laptops, late-model Dell XPS) advertise `\_S3` packages in their
// DSDT but the firmware refuses to honour the SLP_TYP write — the
// machine hangs or silently no-ops. With this quirk armed, S3 entry
// is replaced by the s2idle preparation sequence (which the kernel
// MWAIT loop then completes).
if state == 3
&& self
.system_quirks
.contains(redox_driver_sys::quirks::SystemQuirkFlags::FORCE_S2IDLE)
{
log::info!(
"set_global_s_state(3): FORCE_S2IDLE quirk active, routing S3 entry to s2idle"
);
self.enter_s2idle();
return;
}
let fadt = match self.fadt() {
Some(fadt) => fadt,
None => {
@@ -1122,16 +1190,28 @@ impl AcpiContext {
);
Pio::<u16>::new(port_a).write(val_a);
// 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.
// PM1b write: skipped when (a) the FADT reports no PM1b block
// (the standard "single PM1 block" case), OR (b) the host's
// DMI matches the `no_legacy_pm1b` system quirk — LG Gram and
// similar laptops report a non-zero pm1b_control_block in their
// FADT but the register does not exist on the platform, and
// writes to it have been observed to wedge the controller.
// The quirk is belt-and-braces alongside the FADT check.
let skip_pm1b_quirk = self
.system_quirks
.contains(redox_driver_sys::quirks::SystemQuirkFlags::NO_LEGACY_PM1B);
let port_b = fadt.pm1b_control_block as u16;
if port_b != 0 {
if port_b != 0 && !skip_pm1b_quirk {
log::warn!(
"Sleep S{} with ACPI outw(0x{:X}, 0x{:X})",
state, port_b, val_b
);
Pio::<u16>::new(port_b).write(val_b);
} else if port_b != 0 && skip_pm1b_quirk {
log::info!(
"Sleep S{}: skipping PM1b write at 0x{:X} due to no_legacy_pm1b quirk",
state, port_b
);
}
}
+6 -3
View File
@@ -729,9 +729,12 @@ pub const DMI_FIELDS: &[&str] = &[
"ec_firmware_release",
];
/// Try to load an existing `/scheme/acpi/dmi` cache (if another
/// process already exposed one). This is unused at the moment but
/// kept as a stub for future kernel-side SMBIOS scheme support.
/// Read the `/scheme/acpi/dmi` cache served by another process (typically
/// acpid itself, after `dmi::scan()` populated the in-memory copy and the
/// scheme handler started exposing it). This is the round-trip counterpart
/// to `to_match_lines` — useful for tests that need to verify what the
/// scheme is actually serving, and for any future caller that wants to
/// read DMI from the scheme rather than from a fresh firmware scan.
#[allow(dead_code)]
pub fn try_load_existing() -> Option<DmiInfo> {
let mut file = File::open("/scheme/acpi/dmi").ok()?;
+6
View File
@@ -17,5 +17,11 @@ common = { path = "../../common" }
daemon = { path = "../../../daemon" }
inputd = { path = "../../inputd" }
# Quirks system: ps2d consumes KBD_DEACTIVATE_FIXUP (skip SetDefaultsDisable
# 0xF5 command on affected laptops — LG Gram, some Dell/HP/Lenovo keyboards
# drop keys or wedge the controller when that command is sent). The flag is
# loaded at init from /scheme/acpi/dmi via the system_quirks() helper.
redox-driver-sys = { path = "../../../../../recipes/drivers/redox-driver-sys/source" }
[lints]
workspace = true
+17 -2
View File
@@ -385,7 +385,7 @@ impl Ps2 {
Ok(())
}
pub fn init(&mut self) -> Result<(), Error> {
pub fn init(&mut self, kbd_deactivate_fixup: bool) -> Result<(), Error> {
// Linux i8042_controller_check(): verify controller is present by
// flushing any stale data. A stuck output buffer means no controller.
self.flush();
@@ -468,8 +468,23 @@ impl Ps2 {
self.flush();
// Linux i8042_controller_init() step 2: set keyboard defaults
// (disable scanning so keyboard doesn't send scancodes during init)
// (disable scanning so keyboard doesn't send scancodes during init).
//
// Skip on platforms with the `kbd_deactivate_fixup` DMI quirk
// (LG Gram + a handful of Dell/HP/Lenovo laptops per Linux
// `drivers/input/keyboard/atkbd.c` `keyboard_broken[]` and
// `atkbd_deactivate_input` tables). On those platforms the
// ATKBD_CMD_RESET_DIS (0xF5) command wedges the controller or
// causes silent key drops until the next reset. The keyboard
// still works without the explicit disable — it just may emit
// a few stale scancodes that we drain via flush() above.
self.retry(format_args!("keyboard defaults"), 4, |x| {
if kbd_deactivate_fixup {
log::info!(
"ps2d: skipping SetDefaultsDisable (0xF5) due to kbd_deactivate_fixup quirk"
);
return Ok(0xFA);
}
let b = x.keyboard_command(KeyboardCommand::SetDefaultsDisable)?;
if b != 0xFA {
error!("keyboard failed to set defaults: {:02X}", b);
+13 -1
View File
@@ -94,11 +94,23 @@ fn daemon(daemon: daemon::Daemon) -> ! {
daemon.ready();
// Load machine-wide system quirk flags from /scheme/acpi/dmi (served by
// acpid). The flags drive consumer behaviour in this daemon — currently
// KBD_DEACTIVATE_FIXUP (skip SetDefaultsDisable / 0xF5 during keyboard
// init on LG Gram + similar laptops). Empty set on platforms without
// DMI or without matching quirk rules; ps2d treats that as "no quirks".
let system_quirks = redox_driver_sys::quirks::system_quirks();
let kbd_deactivate_fixup = system_quirks
.contains(redox_driver_sys::quirks::SystemQuirkFlags::KBD_DEACTIVATE_FIXUP);
if !system_quirks.is_empty() {
log::info!("ps2d: loaded system quirks: {:?}", system_quirks);
}
log::info!(
"ps2d: registered producer handle, listening on serio/0 (keyboard) and serio/1 (mouse)"
);
let mut ps2d = Ps2d::new(keyboard_input, mouse_input, time_file);
let mut ps2d = Ps2d::new(keyboard_input, mouse_input, time_file, kbd_deactivate_fixup);
let mut data = [0; 256];
for event_res in event_queue {
+7 -2
View File
@@ -64,9 +64,14 @@ pub struct Ps2d {
}
impl Ps2d {
pub fn new(keyboard_input: InputProducer, mouse_input: InputProducer, time_file: File) -> Self {
pub fn new(
keyboard_input: InputProducer,
mouse_input: InputProducer,
time_file: File,
kbd_deactivate_fixup: bool,
) -> Self {
let mut ps2 = Ps2::new();
if let Err(err) = ps2.init() {
if let Err(err) = ps2.init(kbd_deactivate_fixup) {
log::error!("ps2d: controller init failed: {:?}", err);
}