quirks: Platform DMI dispatch infrastructure + 31-entry data file (R13)

Phase R13 (2026-06-07) — Laptop/Embedded DMI quirks. The
data side lands now; consumer wiring in inputd,
thermald, and redbear-upower is a follow-up.

Changes:

  1. PlatformDmiQuirkFlags (mod.rs:286) with 7 bits:
     TOUCHSCREEN, HOTKEY, ACCELEROMETER, ALS,
     TABLET_MODE, BATTERY, PROXIMITY.

  2. PlatformSubsystem enum (mod.rs:316) — dispatch label
     for TOML and consumers. from_name() for parsing,
     as_str() for logging, flag_bit() for converting to
     the bitflags.

  3. PlatformDmiQuirkRule (dmi.rs:566) — DMI match +
     subsystem. Each entry fires one subsystem.

  4. load_platform_dmi_quirks() (dmi.rs:583) — reads live
     SMBIOS, returns Vec<PlatformDmiQuirkRule> of all
     rules that fire. Falls back to empty vector if DMI
     data is unavailable.

  5. read_toml_platform_dmi_entries + parse_platform_dmi_toml
     (toml_loader.rs) — new [[platform_dmi_quirk]] TOML
     table with  sub-table +  string.
     Unknown subsystem names log a warning and skip.

  6. 1 new unit test: phase_r13_platform_subsystem_from_name_round_trip
     exercises all 7 subsystems. 124/124 tests pass.

  7. quirks.d/80-platform-x86.toml (201 lines) — 31 DMI
     entries covering:
       touchscreen (3): Chuwi Hi8 / Hi8 Pro / Hi10 Plus
       tablet_mode (8): Acer, Asus, Lenovo convertibles
       hotkey (12): Dynabook, GPD, AYA NEO, AYN, OneXPlayer,
                     Valve Steam Deck family
       accelerometer (5): GPD WIN series, AYA NEO 2, Valve
       battery (2): Samsung Galaxy Book, Chuwi Hi10 Plus
     Targeted at Red Bear's 2026 hardware scope. The full
     Linux 7.1 platform/x86 DMI surface is ~1153 entries;
     this is a focused subset that maps to actual Red Bear
     targets.

cargo test: 124/124 (was 123, +1 for the new test).
cargo check: clean.

Consumer wiring: load_platform_dmi_quirks() is callable
today from inputd, thermald, redbear-upower. Each
consumer can filter on rule.subsystem to dispatch the
appropriate behavior. This is a follow-up commit.
This commit is contained in:
2026-06-07 21:41:07 +03:00
parent 87ea8a9acf
commit 00e1c9ea16
4 changed files with 397 additions and 4 deletions
@@ -1,5 +1,5 @@
use super::{ use super::{
toml_loader, AcpiQuirkFlags, DrmPanelOrientation, PciQuirkFlags, toml_loader, AcpiQuirkFlags, DrmPanelOrientation, PciQuirkFlags, PlatformSubsystem,
XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID, XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID,
}; };
use crate::pci::PciDeviceInfo; use crate::pci::PciDeviceInfo;
@@ -543,6 +543,36 @@ pub fn load_drm_panel_orientation() -> DrmPanelOrientation {
.unwrap_or(DrmPanelOrientation::Normal) .unwrap_or(DrmPanelOrientation::Normal)
} }
/// One DMI-conditioned platform / subsystem dispatch rule.
///
/// Phase R13 (2026-06-07). Sourced from Linux 7.1
/// `drivers/platform/x86/*` DMI tables (touchscreen_dmi.c,
/// wireless-hotkey.c, etc.).
#[derive(Clone, Debug)]
pub struct PlatformDmiQuirkRule {
pub dmi_match: DmiMatchRule,
pub subsystem: PlatformSubsystem,
}
/// Look up the platform DMI rules that fire for the host system,
/// grouped by subsystem. Returns an empty vector if DMI data is
/// unavailable or no rule matches.
pub fn load_platform_dmi_quirks() -> Vec<PlatformDmiQuirkRule> {
let dmi_info = match read_dmi_info() {
Ok(info) => info,
Err(()) => return Vec::new(),
};
let mut out = Vec::new();
for rule in toml_loader::read_toml_platform_dmi_entries()
.unwrap_or_default()
{
if rule.dmi_match.matches(&dmi_info) {
out.push(rule);
}
}
out
}
/// Walk a slice of `DmiAcpiQuirkRule` and OR-accumulate the flags /// Walk a slice of `DmiAcpiQuirkRule` and OR-accumulate the flags
/// of every rule whose DMI match succeeds. Mirrors /// of every rule whose DMI match succeeds. Mirrors
/// `apply_dmi_xhci_quirk_rules` (R7-B) and `apply_dmi_pci_quirks`. /// `apply_dmi_xhci_quirk_rules` (R7-B) and `apply_dmi_pci_quirks`.
@@ -888,4 +918,25 @@ mod tests {
); );
assert!(DrmPanelOrientation::from_name("Bogus").is_none()); assert!(DrmPanelOrientation::from_name("Bogus").is_none());
} }
/// Phase R13 — `PlatformSubsystem::from_name` round-trips all
/// seven subsystem identifiers.
#[test]
fn phase_r13_platform_subsystem_from_name_round_trip() {
for name in [
"touchscreen",
"hotkey",
"accelerometer",
"als",
"tablet_mode",
"battery",
"proximity",
] {
assert_eq!(
PlatformSubsystem::from_name(name).unwrap().as_str(),
name
);
}
assert!(PlatformSubsystem::from_name("not_a_subsystem").is_none());
}
} }
@@ -291,6 +291,82 @@ impl DrmPanelOrientation {
} }
} }
bitflags::bitflags! {
/// Per-subsystem platform DMI quirk flags. Each bit names a
/// consumer subsystem that the DMI rule fires for; consumers
/// (inputd, acpid, thermald, etc.) read the bit and react
/// accordingly.
///
/// Phase R13 (2026-06-07) — initial set covers the
/// sub-subsystems Red Bear actively consumes from
/// `drivers/platform/x86/`. New bits can be added as
/// consumer wiring lands.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PlatformDmiQuirkFlags: u64 {
const TOUCHSCREEN = 1 << 0; // drivers/platform/x86/touchscreen_dmi.c
const HOTKEY = 1 << 1; // drivers/platform/x86/wireless-hotkey.c
const ACCELEROMETER = 1 << 2; // drivers/platform/x86/dual_accel_detect.h
const ALS = 1 << 3; // drivers/platform/x86/x86-android-tablets/ — ALS
const TABLET_MODE = 1 << 4; // convertible / 360° hinge detection
const BATTERY = 1 << 5; // battery reporting quirks (separate from ACPI)
const PROXIMITY = 1 << 6; // proximity sensor quirks
}
}
/// Sub-system dispatch label used in `[[platform_dmi_quirk]]` TOML
/// entries and by consumers to filter the flag set. Stable
/// string identifiers (lowercase snake_case) so the TOML side
/// is robust against enum reorderings.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PlatformSubsystem {
Touchscreen,
Hotkey,
Accelerometer,
Als,
TabletMode,
Battery,
Proximity,
}
impl PlatformSubsystem {
pub fn from_name(name: &str) -> Option<Self> {
match name {
"touchscreen" => Some(Self::Touchscreen),
"hotkey" => Some(Self::Hotkey),
"accelerometer" => Some(Self::Accelerometer),
"als" => Some(Self::Als),
"tablet_mode" => Some(Self::TabletMode),
"battery" => Some(Self::Battery),
"proximity" => Some(Self::Proximity),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Touchscreen => "touchscreen",
Self::Hotkey => "hotkey",
Self::Accelerometer => "accelerometer",
Self::Als => "als",
Self::TabletMode => "tablet_mode",
Self::Battery => "battery",
Self::Proximity => "proximity",
}
}
pub fn flag_bit(&self) -> PlatformDmiQuirkFlags {
match self {
Self::Touchscreen => PlatformDmiQuirkFlags::TOUCHSCREEN,
Self::Hotkey => PlatformDmiQuirkFlags::HOTKEY,
Self::Accelerometer => PlatformDmiQuirkFlags::ACCELEROMETER,
Self::Als => PlatformDmiQuirkFlags::ALS,
Self::TabletMode => PlatformDmiQuirkFlags::TABLET_MODE,
Self::Battery => PlatformDmiQuirkFlags::BATTERY,
Self::Proximity => PlatformDmiQuirkFlags::PROXIMITY,
}
}
}
/// Wildcard value for PCI ID matching. /// Wildcard value for PCI ID matching.
pub const PCI_QUIRK_ANY_ID: u16 = 0xFFFF; pub const PCI_QUIRK_ANY_ID: u16 = 0xFFFF;
@@ -1,8 +1,9 @@
use super::{ use super::{
dmi::{self, DmiAcpiQuirkRule, DmiDrmPanelQuirkRule, DmiInfo, DmiMatchRule, DmiPciQuirkRule, DmiXhciQuirkRule}, dmi::{self, DmiAcpiQuirkRule, DmiDrmPanelQuirkRule, DmiInfo, DmiMatchRule, DmiPciQuirkRule, DmiXhciQuirkRule, PlatformDmiQuirkRule},
AcpiQuirkFlags, DrmPanelOrientation, HidQuirkEntry, HidQuirkFlags, MaskWidth, AcpiQuirkFlags, DrmPanelOrientation, HidQuirkEntry, HidQuirkFlags, MaskWidth,
PciQuirkEntry, PciQuirkFlags, PciQuirkLookup, PciQuirkPhase, QuirkAction, UsbQuirkEntry, PciQuirkEntry, PciQuirkFlags, PciQuirkLookup, PciQuirkPhase, PlatformSubsystem,
UsbQuirkFlags, XhciControllerQuirk, XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID, QuirkAction, UsbQuirkEntry, UsbQuirkFlags, XhciControllerQuirk,
XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID,
}; };
use crate::pci::PciDeviceInfo; use crate::pci::PciDeviceInfo;
use std::borrow::Cow; use std::borrow::Cow;
@@ -331,6 +332,70 @@ pub(crate) fn load_drm_panel_orientation(
Ok(DrmPanelOrientation::Normal) Ok(DrmPanelOrientation::Normal)
} }
pub(crate) fn read_toml_platform_dmi_entries() -> std::io::Result<Vec<PlatformDmiQuirkRule>> {
let mut entries = Vec::new();
for path in sorted_toml_files(QUIRKS_DIR)? {
let path_str = path.display().to_string();
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
log::warn!("quirks: failed to read {path_str}: {e}");
continue;
}
};
let doc = match content.parse::<toml::Value>() {
Ok(d) => d,
Err(e) => {
log::warn!("quirks: failed to parse {path_str}: {e}");
continue;
}
};
parse_platform_dmi_toml(&doc, &mut entries, &path_str);
}
Ok(entries)
}
fn parse_platform_dmi_toml(
doc: &toml::Value,
out: &mut Vec<PlatformDmiQuirkRule>,
path: &str,
) {
let Some(arr) = doc.get("platform_dmi_quirk").and_then(|v| v.as_array()) else {
return;
};
for item in arr {
let Some(table) = item.as_table() else {
log::warn!("quirks: {path}: platform_dmi_quirk entry is not a table, skipping");
continue;
};
let Some(match_table) = table.get("match").and_then(|v| v.as_table()) else {
log::warn!(
"quirks: {path}: platform_dmi_quirk entry is missing match table, skipping"
);
continue;
};
let Some(dmi_match) = parse_dmi_match_rule(match_table, path) else {
continue;
};
let Some(subsystem_str) = table.get("subsystem").and_then(|v| v.as_str()) else {
log::warn!(
"quirks: {path}: platform_dmi_quirk entry is missing subsystem string, skipping"
);
continue;
};
let Some(subsystem) = PlatformSubsystem::from_name(subsystem_str) else {
log::warn!(
"quirks: {path}: unknown platform_dmi_quirk subsystem {subsystem_str:?}, skipping"
);
continue;
};
out.push(PlatformDmiQuirkRule {
dmi_match,
subsystem,
});
}
}
fn bounded_u16(val: &toml::Value, field: &str, path: &str) -> Option<u16> { fn bounded_u16(val: &toml::Value, field: &str, path: &str) -> Option<u16> {
match val.as_integer() { match val.as_integer() {
Some(v) => u16::try_from(v).ok().or_else(|| { Some(v) => u16::try_from(v).ok().or_else(|| {
@@ -0,0 +1,201 @@
# Platform DMI dispatch rules — DMI-based, subsystem-tagged.
# Mined from Linux 7.1 `drivers/platform/x86/*` DMI tables.
# Each `[[platform_dmi_quirk]]` entry maps a DMI fingerprint to
# a single `subsystem` (touchscreen / hotkey / accelerometer /
# als / tablet_mode / battery / proximity). Consumers
# (inputd, acpid, thermald, redbear-upower, etc.) read the
# firing rules and react accordingly.
#
# Phase R13 (2026-06-07) initial commit. The data file is
# sparse by design — the audit estimated ~1153 Linux 7.1
# entries, but Red Bear's hardware scope is much narrower.
# Focus on hardware that ships in 2026 and that Red Bear
# actually targets: Framework, GPD, AYANEO, AYN, Dell,
# Lenovo, Asus, Valve, Chuwi, Acer.
#
# Consumers currently in the tree that could read this:
# - inputd: touchscreen, hotkey, accelerometer, tablet_mode
# - acpid: button, lid (related, not a direct match)
# - thermald: als (ambient light → thermal profile)
# - redbear-upower: battery
# Framework Laptop 13 / 16 — well-behaved; no quirks needed.
# Listed here as documentation of the absence of quirks.
# Chuwi Hi8 (CWI506 / ilife S806) — touchscreen needs I2C-HID fallback
[[platform_dmi_quirk]]
subsystem = "touchscreen"
match.sys_vendor = "ilife"
match.product_name = "S806"
# Chuwi Hi8 Pro (CWI513 / Hampoo X1D3_C806N) — same family
[[platform_dmi_quirk]]
subsystem = "touchscreen"
match.sys_vendor = "Hampoo"
match.product_name = "X1D3_C806N"
# Chuwi Hi10 Plus (CWI527) — touchscreen quirks
[[platform_dmi_quirk]]
subsystem = "touchscreen"
match.board_vendor = "Hampoo"
match.product_name = "Hi10 Pro tablet"
# Acer Switch V 10 (SW5-017) — convertible / tablet_mode
[[platform_dmi_quirk]]
subsystem = "tablet_mode"
match.sys_vendor = "Acer"
match.product_name = "SW5-017"
# Acer One 10 (S1003) — convertible / tablet_mode
[[platform_dmi_quirk]]
subsystem = "tablet_mode"
match.sys_vendor = "Acer"
match.product_name = "One S1003"
# Asus T100HAN — convertible / tablet_mode
[[platform_dmi_quirk]]
subsystem = "tablet_mode"
match.sys_vendor = "ASUSTeK COMPUTER INC."
match.product_name = "T100HAN"
# Asus T101HA / T103HAF — convertible / tablet_mode
[[platform_dmi_quirk]]
subsystem = "tablet_mode"
match.sys_vendor = "ASUSTeK COMPUTER INC."
match.product_name = "T101HA"
[[platform_dmi_quirk]]
subsystem = "tablet_mode"
match.sys_vendor = "ASUSTeK COMPUTER INC."
match.product_name = "T103HAF"
# Lenovo Ideapad Miix 320 (80SG) — convertible
[[platform_dmi_quirk]]
subsystem = "tablet_mode"
match.sys_vendor = "LENOVO"
match.product_name = "80SG"
# Lenovo Ideapad D330 (80XF) — convertible
[[platform_dmi_quirk]]
subsystem = "tablet_mode"
match.sys_vendor = "LENOVO"
match.product_name = "80XF"
# Lenovo Yoga Book (YB1-X91) — convertible + Wacom touchscreen
[[platform_dmi_quirk]]
subsystem = "tablet_mode"
match.sys_vendor = "Intel Corporation"
match.product_name = "CHERRYVIEW D1 PLATFORM"
# Dynabook K50 — wireless hotkey quirks
[[platform_dmi_quirk]]
subsystem = "hotkey"
match.sys_vendor = "Dynabook Inc."
match.product_name = "dynabook K50/FR"
# GPD MicroPC — wireless hotkey (airplane-mode)
[[platform_dmi_quirk]]
subsystem = "hotkey"
match.sys_vendor = "GPD"
match.product_name = "MicroPC"
# GPD Pocket 2/3 — wireless hotkey
[[platform_dmi_quirk]]
subsystem = "hotkey"
match.sys_vendor = "GPD"
match.product_name = "Pocket 2"
[[platform_dmi_quirk]]
subsystem = "hotkey"
match.sys_vendor = "GPD"
match.product_name = "Pocket 3"
# AYA NEO 2 / AIR / SLIDE — wireless hotkey (airplane-mode)
[[platform_dmi_quirk]]
subsystem = "hotkey"
match.sys_vendor = "AYANEO"
match.product_name = "AYANEO 2"
[[platform_dmi_quirk]]
subsystem = "hotkey"
match.sys_vendor = "AYANEO"
match.product_name = "AIR"
[[platform_dmi_quirk]]
subsystem = "hotkey"
match.sys_vendor = "AYANEO"
match.product_name = "SLIDE"
# AYN Loki Max / Zero — wireless hotkey
[[platform_dmi_quirk]]
subsystem = "hotkey"
match.sys_vendor = "ayn"
match.product_name = "Loki Max"
[[platform_dmi_quirk]]
subsystem = "hotkey"
match.sys_vendor = "ayn"
match.product_name = "Loki Zero"
# OneXPlayer — wireless hotkey
[[platform_dmi_quirk]]
subsystem = "hotkey"
match.sys_vendor = "ONE-NETBOOK TECHNOLOGY CO., LTD."
match.product_name = "ONE XPLAYER"
# Valve Steam Deck (Jupiter) — wireless hotkey
[[platform_dmi_quirk]]
subsystem = "hotkey"
match.sys_vendor = "Valve"
match.product_name = "Jupiter"
# Valve Steam Deck OLED (Galileo)
[[platform_dmi_quirk]]
subsystem = "hotkey"
match.sys_vendor = "Valve"
match.product_name = "Galileo"
# GPD WIN2/3/4 — accelerometer (auto-rotation in handheld mode)
[[platform_dmi_quirk]]
subsystem = "accelerometer"
match.sys_vendor = "GPD"
match.product_name = "WIN2"
[[platform_dmi_quirk]]
subsystem = "accelerometer"
match.sys_vendor = "GPD"
match.product_name = "G1618-03"
[[platform_dmi_quirk]]
subsystem = "accelerometer"
match.sys_vendor = "GPD"
match.product_name = "G1618-04"
# AYA NEO 2 — accelerometer
[[platform_dmi_quirk]]
subsystem = "accelerometer"
match.sys_vendor = "AYANEO"
match.product_name = "AYANEO 2"
# Valve Steam Deck — accelerometer
[[platform_dmi_quirk]]
subsystem = "accelerometer"
match.sys_vendor = "Valve"
match.product_name = "Jupiter"
[[platform_dmi_quirk]]
subsystem = "accelerometer"
match.sys_vendor = "Valve"
match.product_name = "Galileo"
# Samsung Galaxy Book 10.6 — battery reporting quirks
[[platform_dmi_quirk]]
subsystem = "battery"
match.sys_vendor = "SAMSUNG ELECTRONICS CO., LTD."
match.product_name = "Galaxy Book 10.6"
# Chuwi Hi10 Plus — battery quirk (BIX broken)
[[platform_dmi_quirk]]
subsystem = "battery"
match.board_vendor = "Hampoo"
match.product_name = "Hi10 Pro tablet"