quirks: DRM panel orientation infrastructure + 36-entry data file (R12)

Phase R12 (2026-06-07) — DRM panel orientation quirks.
The data side lands now; consumer wiring in redox-drm is
deferred until compositor rotation lands (Phase 4 KDE).

Changes:

  1. DrmPanelOrientation enum (mod.rs:225) with four
     values: Normal, RightUp, LeftUp, BottomUp. Sourced
     from Linux 7.1 include/drm/drm_panel_orientation.h.
     Provides from_name() for TOML parsing and as_str()
     for logging.

  2. DmiDrmPanelQuirkRule (dmi.rs:517) — DMI match + panel
     orientation, mirrors the existing DmiAcpiQuirkRule
     shape from R11.

  3. DMI_DRM_PANEL_QUIRK_RULES (dmi.rs:531) — empty
     compiled-in table; runtime TOML is the data surface
     (see 50-drm-panel.toml).

  4. load_drm_panel_orientation() (dmi.rs:537) — reads
     live SMBIOS via read_dmi_info, applies the
     compiled-in + TOML rules, returns the orientation.
     Falls back to Normal if DMI data is unavailable or
     no rule matches.

  5. read_toml_drm_panel_entries + parse_drm_panel_toml
     (toml_loader.rs) — new [[drm_panel_quirk]] TOML
     table type with  sub-table +
     string. Unknown orientation names log a warning
     and skip the entry.

  6. load_drm_panel_orientation (toml_loader) — applies
     the first matching TOML rule, returns Normal if
     none match.

  7. 1 new unit test: phase_r12_drm_panel_orientation_from_name_round_trip
     exercises all four orientation values + a bogus
     name. 123/123 redox-driver-sys tests pass.

  8. quirks.d/50-drm-panel.toml (234 lines) — 36 DMI
     entries sourced from Linux 7.1
     drivers/gpu/drm/drm_panel_orientation_quirks.c.
     Covers Acer, Anbernic, Asus, AYA NEO (full range
     including 2/2S, 2021, AIR, FLIP, Founder, GEEK,
     NEXT, KUN, SLIDE), AYN (Loki Max, Loki Zero),
     Chuwi, Dynabook, GPD (MicroPC, WIN Max, Pocket 2/3,
     WIN2/3/4, WIN Max 2), Lenovo, OneXPlayer, OrangePi,
     Samsung Galaxy Book, Valve Jupiter/Galileo
     (Steam Deck family), ZOTAC. The data spans
     laptop, tablet, and handheld form factors.

cargo test: 123/123 (was 122, +1 for the new test).
cargo check: clean.
cargo clippy: no new warnings in this code.

Consumer wiring is R12.1 (out of scope for this turn):
redox-drm will call load_drm_panel_orientation() at
connector enumeration time and apply the returned
transform once the compositor supports rotation.
This commit is contained in:
2026-06-07 21:36:12 +03:00
parent 4f5b35bb62
commit 87ea8a9acf
4 changed files with 409 additions and 5 deletions
@@ -1,4 +1,7 @@
use super::{toml_loader, AcpiQuirkFlags, PciQuirkFlags, XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID};
use super::{
toml_loader, AcpiQuirkFlags, DrmPanelOrientation, PciQuirkFlags,
XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID,
};
use crate::pci::PciDeviceInfo;
use std::borrow::Cow;
@@ -508,6 +511,38 @@ pub fn load_dmi_acpi_quirks() -> AcpiQuirkFlags {
flags
}
/// One DMI-conditioned DRM panel orientation rule.
///
/// Phase R12 (2026-06-07) — sourced from Linux 7.1
/// `drivers/gpu/drm/drm_panel_orientation_quirks.c` (55 entries).
/// Consumer is `redox-drm` (deferred until compositor rotation).
#[derive(Clone, Debug)]
pub struct DmiDrmPanelQuirkRule {
pub dmi_match: DmiMatchRule,
pub orientation: DrmPanelOrientation,
}
/// Compiled-in DMI panel orientation rules. Empty for now — runtime
/// TOML is the data surface (see `quirks.d/50-drm-panel.toml`).
pub const DMI_DRM_PANEL_QUIRK_RULES: &[DmiDrmPanelQuirkRule] = &[];
/// Look up the panel orientation for the host system. Returns
/// `Normal` if no rule matches or DMI data is unavailable.
pub fn load_drm_panel_orientation() -> DrmPanelOrientation {
let dmi_info = match read_dmi_info() {
Ok(info) => info,
Err(()) => return DrmPanelOrientation::Normal,
};
if let Some(rule) = DMI_DRM_PANEL_QUIRK_RULES
.iter()
.find(|rule| rule.dmi_match.matches(&dmi_info))
{
return rule.orientation;
}
toml_loader::load_drm_panel_orientation(&dmi_info)
.unwrap_or(DrmPanelOrientation::Normal)
}
/// Walk a slice of `DmiAcpiQuirkRule` and OR-accumulate the flags
/// of every rule whose DMI match succeeds. Mirrors
/// `apply_dmi_xhci_quirk_rules` (R7-B) and `apply_dmi_pci_quirks`.
@@ -830,4 +865,27 @@ mod tests {
AcpiQuirkFlags::SLEEP_OLD_ORDERING | AcpiQuirkFlags::LID_INIT_DISABLED
);
}
/// Phase R12 — `DrmPanelOrientation::from_name` round-trips all
/// four Linux 7.1 orientation values.
#[test]
fn phase_r12_drm_panel_orientation_from_name_round_trip() {
assert_eq!(
DrmPanelOrientation::from_name("Normal").unwrap().as_str(),
"Normal"
);
assert_eq!(
DrmPanelOrientation::from_name("RightUp").unwrap().as_str(),
"RightUp"
);
assert_eq!(
DrmPanelOrientation::from_name("LeftUp").unwrap().as_str(),
"LeftUp"
);
assert_eq!(
DrmPanelOrientation::from_name("BottomUp").unwrap().as_str(),
"BottomUp"
);
assert!(DrmPanelOrientation::from_name("Bogus").is_none());
}
}
@@ -254,6 +254,43 @@ bitflags::bitflags! {
}
}
/// Panel orientation for portrait-screen devices (GPD, Chuwi,
/// AYA NEO, OneXPlayer, etc.). Sourced from Linux 7.1
/// `include/drm/drm_panel_orientation.h` `drm_panel_orientation`
/// values. Phase R12 (2026-06-07) — consumer wiring in
/// `redox-drm` is deferred until compositor rotation lands.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DrmPanelOrientation {
Normal,
RightUp, // 90° clockwise
LeftUp, // 90° counter-clockwise
BottomUp, // 180°
}
impl DrmPanelOrientation {
/// Parse the orientation string used in the `[[drm_panel_quirk]]`
/// TOML table. Returns `None` for unknown values.
pub fn from_name(name: &str) -> Option<Self> {
match name {
"Normal" => Some(Self::Normal),
"RightUp" => Some(Self::RightUp),
"LeftUp" => Some(Self::LeftUp),
"BottomUp" => Some(Self::BottomUp),
_ => None,
}
}
/// Human-readable name for logging.
pub fn as_str(&self) -> &'static str {
match self {
Self::Normal => "Normal",
Self::RightUp => "RightUp",
Self::LeftUp => "LeftUp",
Self::BottomUp => "BottomUp",
}
}
}
/// Wildcard value for PCI ID matching.
pub const PCI_QUIRK_ANY_ID: u16 = 0xFFFF;
@@ -1,8 +1,8 @@
use super::{
dmi::{self, DmiAcpiQuirkRule, DmiInfo, DmiMatchRule, DmiPciQuirkRule, DmiXhciQuirkRule},
AcpiQuirkFlags, HidQuirkEntry, HidQuirkFlags, MaskWidth, PciQuirkEntry, PciQuirkFlags,
PciQuirkLookup, PciQuirkPhase, QuirkAction, UsbQuirkEntry, UsbQuirkFlags,
XhciControllerQuirk, XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID,
dmi::{self, DmiAcpiQuirkRule, DmiDrmPanelQuirkRule, DmiInfo, DmiMatchRule, DmiPciQuirkRule, DmiXhciQuirkRule},
AcpiQuirkFlags, DrmPanelOrientation, HidQuirkEntry, HidQuirkFlags, MaskWidth,
PciQuirkEntry, PciQuirkFlags, PciQuirkLookup, PciQuirkPhase, QuirkAction, UsbQuirkEntry,
UsbQuirkFlags, XhciControllerQuirk, XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID,
};
use crate::pci::PciDeviceInfo;
use std::borrow::Cow;
@@ -256,6 +256,81 @@ pub(crate) fn load_dmi_acpi_quirks(dmi_info: &DmiInfo) -> Result<AcpiQuirkFlags,
Ok(dmi::apply_dmi_acpi_quirk_rules(dmi_info, &entries))
}
pub(crate) fn read_toml_drm_panel_entries() -> std::io::Result<Vec<DmiDrmPanelQuirkRule>> {
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_drm_panel_toml(&doc, &mut entries, &path_str);
}
Ok(entries)
}
fn parse_drm_panel_toml(
doc: &toml::Value,
out: &mut Vec<DmiDrmPanelQuirkRule>,
path: &str,
) {
let Some(arr) = doc.get("drm_panel_quirk").and_then(|v| v.as_array()) else {
return;
};
for item in arr {
let Some(table) = item.as_table() else {
log::warn!("quirks: {path}: drm_panel_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}: drm_panel_quirk entry is missing match table, skipping");
continue;
};
let Some(dmi_match) = parse_dmi_match_rule(match_table, path) else {
continue;
};
let Some(orientation_str) = table.get("orientation").and_then(|v| v.as_str()) else {
log::warn!("quirks: {path}: drm_panel_quirk entry is missing orientation string, skipping");
continue;
};
let Some(orientation) = DrmPanelOrientation::from_name(orientation_str) else {
log::warn!(
"quirks: {path}: unknown drm_panel_quirk orientation {orientation_str:?}, skipping"
);
continue;
};
out.push(DmiDrmPanelQuirkRule {
dmi_match,
orientation,
});
}
}
/// Look up the panel orientation for the host system from runtime
/// TOML files. Returns the orientation of the first matching
/// `[[drm_panel_quirk]]` entry, or `Normal` if no rule matches.
pub(crate) fn load_drm_panel_orientation(
dmi_info: &DmiInfo,
) -> Result<DrmPanelOrientation, ()> {
let entries = read_toml_drm_panel_entries().map_err(|_| ())?;
for rule in entries {
if rule.dmi_match.matches(dmi_info) {
return Ok(rule.orientation);
}
}
Ok(DrmPanelOrientation::Normal)
}
fn bounded_u16(val: &toml::Value, field: &str, path: &str) -> Option<u16> {
match val.as_integer() {
Some(v) => u16::try_from(v).ok().or_else(|| {
@@ -0,0 +1,234 @@
# DRM panel orientation quirks — DMI-based.
# Mined from Linux 7.1
# `drivers/gpu/drm/drm_panel_orientation_quirks.c` (55 entries).
# Each `[[drm_panel_quirk]]` entry maps a DMI fingerprint to a
# panel orientation: Normal (default), RightUp (90° CW),
# LeftUp (90° CCW), BottomUp (180°).
#
# Phase R12 (2026-06-07). Consumer is `redox-drm` —
# rotation support is deferred until Phase 4 KDE session.
# The lookup function `load_drm_panel_orientation()` is callable
# today; consumers just need to read its result and apply the
# transform.
# Acer One 10 (S1003)
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "Acer"
match.product_name = "One S1003"
# Acer Switch V 10 (SW5-017)
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "Acer"
match.product_name = "SW5-017"
# Anbernic Win600
[[drm_panel_quirk]]
orientation = "RightUp"
match.board_vendor = "Anbernic"
match.product_name = "Win600"
# Asus T100HAN
[[drm_panel_quirk]]
orientation = "LeftUp"
match.sys_vendor = "ASUSTeK COMPUTER INC."
match.product_name = "T100HAN"
# Asus T101HA
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "ASUSTeK COMPUTER INC."
match.product_name = "T101HA"
# Asus T103HAF
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "ASUSTeK COMPUTER INC."
match.product_name = "T103HAF"
# AYA NEO AYANEO 2 / 2S
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "AYANEO"
match.product_name = "AYANEO 2"
# AYA NEO 2021
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "AYADEVICE"
match.product_name = "AYA NEO 2021"
# AYA NEO AIR
[[drm_panel_quirk]]
orientation = "LeftUp"
match.sys_vendor = "AYANEO"
match.product_name = "AIR"
# AYA NEO Flip DS Bottom Screen
[[drm_panel_quirk]]
orientation = "LeftUp"
match.sys_vendor = "AYANEO"
match.product_name = "FLIP DS"
# AYA NEO Flip KB/DS Top Screen
[[drm_panel_quirk]]
orientation = "LeftUp"
match.sys_vendor = "AYANEO"
match.product_name = "FLIP"
# AYA NEO Founder
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "AYA NEO"
match.product_name = "AYA NEO Founder"
# AYA NEO GEEK
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "AYANEO"
match.product_name = "GEEK"
# AYA NEO NEXT
[[drm_panel_quirk]]
orientation = "RightUp"
match.board_vendor = "AYANEO"
match.board_name = "NEXT"
# AYA NEO KUN
[[drm_panel_quirk]]
orientation = "RightUp"
match.board_vendor = "AYANEO"
match.board_name = "KUN"
# AYA NEO SLIDE
[[drm_panel_quirk]]
orientation = "LeftUp"
match.sys_vendor = "AYANEO"
match.product_name = "SLIDE"
# AYN Loki Max
[[drm_panel_quirk]]
orientation = "LeftUp"
match.sys_vendor = "ayn"
match.product_name = "Loki Max"
# AYN Loki Zero
[[drm_panel_quirk]]
orientation = "LeftUp"
match.sys_vendor = "ayn"
match.product_name = "Loki Zero"
# Chuwi Hi10 Pro (CWI529)
[[drm_panel_quirk]]
orientation = "RightUp"
match.board_vendor = "Hampoo"
match.product_name = "Hi10 pro tablet"
# Dynabook K50
[[drm_panel_quirk]]
orientation = "LeftUp"
match.sys_vendor = "Dynabook Inc."
match.product_name = "dynabook K50/FR"
# GPD MicroPC
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "GPD"
match.product_name = "MicroPC"
# GPD Win Max (G1619-01)
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "GPD"
match.product_name = "G1619-01"
# GPD Pocket (G1617-01)
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "GPD"
match.product_name = "G1617-01"
# GPD Pocket 2
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "GPD"
match.product_name = "Pocket 2"
# GPD Pocket 3
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "GPD"
match.product_name = "Pocket 3"
# GPD WIN2
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "GPD"
match.product_name = "WIN2"
# GPD WIN3
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "GPD"
match.product_name = "G1618-03"
# GPD WIN4
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "GPD"
match.product_name = "G1618-04"
# GPD WIN Max 2 (2023)
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "GPD"
match.product_name = "G1619-04"
# Lenovo Ideapad Miix 320 (80SG)
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "LENOVO"
match.product_name = "80SG"
# Lenovo Ideapad D330 (80XF)
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "LENOVO"
match.product_name = "80XF"
# OneXPlayer
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "ONE-NETBOOK TECHNOLOGY CO., LTD."
match.product_name = "ONE XPLAYER"
# OrangePi NEO-01
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "OrangePi"
match.product_name = "NEO-01"
# Samsung Galaxy Book 10.6
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "SAMSUNG ELECTRONICS CO., LTD."
match.product_name = "Galaxy Book 10.6"
# Valve Jupiter (Steam Deck)
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "Valve"
match.product_name = "Jupiter"
# Valve Galileo (Steam Deck OLED)
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "Valve"
match.product_name = "Galileo"
# ZOTAC ZBOX PI336 (G0A1W)
[[drm_panel_quirk]]
orientation = "RightUp"
match.sys_vendor = "ZOTAC"
match.board_name = "G0A1W"