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:
@@ -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"
|
||||
Reference in New Issue
Block a user