diff --git a/local/docs/QUIRKS-IMPROVEMENT-PLAN.md b/local/docs/QUIRKS-IMPROVEMENT-PLAN.md index f675926790..9a6f8842dd 100644 --- a/local/docs/QUIRKS-IMPROVEMENT-PLAN.md +++ b/local/docs/QUIRKS-IMPROVEMENT-PLAN.md @@ -9,6 +9,79 @@ This plan replaces vague “quirks support” follow-up work with a concrete pat 3. reduce duplicated quirk logic, 4. leave DMI and USB device quirks in a maintainable state. +**v2.0 update (2026-07-24):** adds the **universal quirk-consumption model** (§ below) — a +redesign assessment grounded in a full cross-reference of Linux 7.1 quirk consumption across +all 9 quirk families (PCI, USB, HDA, DMI, ATA, NVMe, HID, ACPI, MMC), and the first +implementation of it: the open driver-scoped domain channel (`[[_quirk]]` tables + +`quirks::lookup_driver_quirks`), which also resolves the `audio_*` unknown-flag drift. + +## Universal quirk-consumption model (v2.0 redesign assessment) + +### What the Linux 7.1 cross-reference established + +Linux has **no unified registry** — 9 separate per-subsystem tables. But every one of them +follows the same three-layer invariant: + +``` +[1] MATCH (typed keys: VID/DID, class, subsystem IDs, model strings, DMI, CID/dates) +[2] STORE (flag bits or fixup pointers attached to the device object) +[3] CONSUME (the subsystem CORE or the owning driver, at FIXED lifecycle points) +``` + +Three consumption classes exist: + +| Class | Linux example | Mechanism | +|---|---|---| +| **A. Core lifecycle callbacks** | PCI `DECLARE_PCI_FIXUP_*` (8 passes: early/header/final/enable/resume/...) | Core invokes quirk *functions* at lifecycle points; drivers never look anything up | +| **B. Core flag checks** | USB `usb_detect_quirks`, NVMe `.driver_data`, HID `hid_lookup_quirk`, ATA `ata_dev_quirks` | Flags set on the device object at enumeration; the subsystem core checks them at behavior points | +| **C. Driver-owned domain tables** | HDA `snd_hda_pick_fixup` (5 actions: PRE_PROBE/PROBE/BUILD/INIT/FREE) | The driver matches its OWN table at its own lifecycle points | + +### Red Bear's position (post-cutover, 2026-07-24) + +Red Bear diverges from Linux deliberately and correctly by having **one unified registry** +(`redox-driver-sys`: compiled tables + TOML + DMI) instead of N tables. The registry is the +right base; what was missing is the lifecycle model and the Type-C driver-domain channel. + +**The redesigned model — one registry, three consumption classes:** + +1. **Bind-time (Class B, owned by driver-manager).** Spawn-time quirk decisions: env hints + (`REDBEAR_DRIVER_PCI_IRQ_MODE`, `DISABLE_ACCEL`, `IOMMU_GROUP`, `NUMA_NODE`, + `MSIX_VECTORS`), `NEED_FIRMWARE` deferral, blacklist interplay. Implemented and + QEMU-validated in the cutover. +2. **Core-runtime (Class B, owned by subsystem daemons).** Each subsystem daemon queries the + registry at its own behavior points: `xhcid` (51-flag xHCI table), `redox-drm` + (IRQ/accel), `linux-kpi` C drivers (`pci_has_quirk()`), `acpid` (system quirks), and — + planned — `pcid` for config-space behaviors and `evdevd`/HID for input quirks. +3. **Driver-runtime (Class C, NEW — open domain channel).** Driver-owned domains carried as + string flags keyed by domain name: `[[_quirk]]` TOML tables, matched by + `(vendor, device)` with wildcards, accumulated by + `quirks::lookup_driver_quirks(domain, vid, did) -> Vec`. **New domains need zero + registry code — the table name is the domain key.** First instance: `[[audio_quirk]]` + (`quirks.d/15-audio.toml`) with `quirks::lookup_audio_quirks()` for ihdad's future + integration, mirroring Linux's per-codec HDA fixup model. + +### What this changes vs the v1 plan + +- The v1 plan's "brokered quirk bits from **pcid-spawner**" is **stale** — pcid-spawner is + retired (gated fallback); driver-manager is the bind-time consumer now, and the broker + concept is subsumed by Class 1 above. +- The v1 plan had no answer for driver-specific quirks (audio) — they warned as unknown + flags and were dropped. Class 3 is the answer: carried, queryable, zero-warning. +- Lifecycle beyond spawn (Linux Class A phases: early pre-claim vs enable vs suspend/resume) + remains a **next-milestone** item: `phase = "early"|"enable"` on entries, with + driver-manager consulting the registry during enumeration (early) in addition to spawn + (enable). PM phases wait for the power-management roadmap. + +### Consumption contract per class (who may query what) + +| Consumer | API | Class | +|---|---|---| +| driver-manager (spawn) | `lookup_pci_quirks` → env hints, deferrals | 1 | +| Rust drivers/daemons | `info.quirks()`, `lookup_usb_quirks`, `lookup_xhci_controller_quirks_full`, `lookup_hid_quirks` | 2 | +| C drivers via linux-kpi | `pci_has_quirk()` / `pci_get_quirk_flags()` | 2 | +| Driver-owned domains | `lookup_driver_quirks(domain, vid, did)`, `lookup_audio_quirks(vid, did)` | 3 | +| Tooling (lspci/lsusb/info) | same lookups, display-only | — | + ## Current status snapshot Completed from this plan: @@ -43,9 +116,11 @@ shipped paths. It is based on the current in-tree state of: -- `redox-driver-sys` as the canonical quirks library, -- `pcid-spawner` as an upstream-owned PCI launch broker that now brokers canonical quirks, -- `redox-drm`, `xhcid`, and the amdgpu Redox glue/runtime path as real runtime PCI quirk consumers, +- `redox-driver-sys` as the canonical quirks library (three-layer model: match → store → + consume; typed bitflag domains plus the open driver-scoped domain channel), +- `driver-manager` as the bind-time quirk consumer (spawn env hints, deferrals) — validated + in the 2026-07-23/24 QEMU gate, +- `redox-drm`, `xhcid`, and the amdgpu Redox glue/runtime path as runtime quirk consumers, - `lspci`, `lsusb`, and `redbear-info` as reporting surfaces. ## Reassessment Summary @@ -63,7 +138,6 @@ It is based on the current in-tree state of: - USB quirks now have a first real runtime consumer in `xhcid`, but broader USB-driver adoption is still missing. - The `linux-kpi` bridge now has a first real in-tree C consumer: amdgpu uses it for quirk-aware IRQ expectation logging. Broader C-driver adoption is still missing. -- `pcid-spawner` still synthesizes a partial `PciDeviceInfo` instead of reusing a richer canonical PCI object, because it operates as an upstream-owned broker with a narrow interface. ### What should not be “fixed” in the wrong layer @@ -97,7 +171,9 @@ All other code should either: - **Rust PCI drivers using `redox-driver-sys`** should call `info.quirks()` directly. - **C drivers using `linux-kpi`** should call `pci_has_quirk()` / `pci_get_quirk_flags()` directly in probe/init paths. -- **Upstream base drivers that cannot depend on `redox-driver-sys`** may continue using brokered quirk bits from `pcid-spawner`, but only if that broker is made semantically identical to the canonical library. +- **Driver-owned domain quirks** (audio, future nvme/ata) use + `lookup_driver_quirks(domain, vendor, device)` / `lookup_audio_quirks(vendor, device)` — + see the universal model § above. - **USB device quirks** should be consumed inside `xhcid` device enumeration/configuration logic, not only in tooling. ## Concrete Work Plan diff --git a/local/docs/QUIRKS-SYSTEM.md b/local/docs/QUIRKS-SYSTEM.md index 92c68bd86f..81fefc3572 100644 --- a/local/docs/QUIRKS-SYSTEM.md +++ b/local/docs/QUIRKS-SYSTEM.md @@ -15,6 +15,25 @@ For the current follow-up cleanup and integration roadmap, see ## Architecture +The system follows the same three-layer invariant as every Linux quirk family +(cross-referenced 2026-07-24 against Linux 7.1 PCI/USB/HDA/DMI/ATA/NVMe/HID/ACPI/MMC): + +``` +[1] MATCH typed keys — (vendor, device, subvendor/subdevice, class, revision, DMI) +[2] STORE flag bits (typed domains) or string flags (open driver domains) +[3] CONSUME a fixed consumer at a fixed point — never ad-hoc lookups +``` + +Consumption contract (who consumes, at which point): + +| Class | Consumer | Point | Mechanism | +|---|---|---|---| +| Bind-time | `driver-manager` | probe/spawn | env hints (`REDBEAR_DRIVER_*`), `NEED_FIRMWARE` deferral, blacklist | +| Core-runtime | subsystem daemons (`xhcid`, `redox-drm`, `acpid`, linux-kpi C drivers) | their own behavior points | `info.quirks()`, `lookup_usb_quirks`, `lookup_xhci_controller_quirks_full`, `pci_has_quirk()` | +| Driver-runtime | the owning driver (e.g. ihdad, future nvme/ata) | driver init | `lookup_driver_quirks(domain, vid, did)` — open `[[_quirk]]` tables | + +Lookup flow for typed PCI flags: + ``` Driver probes device └─ PciDeviceInfo::quirks() @@ -28,6 +47,29 @@ All matching entries accumulate via bitwise OR, so broad rules (e.g., "all AMD G need firmware") and narrow rules (e.g., "this specific revision has broken MSI-X") compose naturally. +## Open driver-scoped domains (Type C, 2026-07-24) + +Driver-owned quirk domains follow Linux's Type-C model (driver-owned fixup tables, +e.g. HDA's per-codec fixup lists). The registry matches and carries plain string +flags; the owning driver is the sole consumer and semantic owner: + +```toml +[[audio_quirk]] +vendor = 0x8086 +device = 0x284b +flags = ["audio_force_eapd"] +``` + +- Table name = domain key: any `[[_quirk]]` table works with **zero registry + code** for a new driver domain. +- Match keys: `vendor`, `device` (hex, `0xFFFF` wildcard). +- Query: `redox_driver_sys::quirks::lookup_driver_quirks(domain, vendor, device) + -> Vec`; convenience: `lookup_audio_quirks(vendor, device)`. +- First instance: `quirks.d/15-audio.toml` (HDA quirks: force-EAPD, single-command, + position-fix-LPIB — mirrors Linux `sound/pci/hda` device quirks). ihdad consumes + these when its quirk integration lands; the flags were previously dropped with + "unknown flag" warnings. + ## Quirk Sources ### 1. Compiled-in Tables @@ -470,7 +512,7 @@ cargo test | Q3 | xhcid PCI controller quirks (interrupt + reset delay) | ✅ Done | | Q3 | xhcid xHCI controller quirks — canonical 51-flag table (Linux 7.1 port, ~85 entries) | ✅ Done (2026-07-18, P2-A) | | Q3 | xhcid USB device quirks (descriptor/configuration/BOS handling) | ✅ Done | -| Q3 | pcid-spawner quirk passthrough | ✅ Done | +| Q3 | driver-manager bind-time consumption (spawn env hints, NEED_FIRMWARE deferral) | ✅ Done — QEMU-validated 2026-07-23/24 | | Q3 | linux-kpi quirk flag bridge | ✅ Done | | Q3 | amdgpu linux-kpi quirk consumption | ✅ Done | | Q3 | redbear-info --quirks display | ✅ Done | @@ -497,7 +539,11 @@ the honest breakdown. - amdgpu: startup firmware requirement enforced at the Rust DRM boundary, with real quirk-aware runtime logging for `NO_ASPM`, `NEED_IOMMU`, `NO_MSI`, `NO_MSIX` **Infrastructure (data flows, reporting, and partial integration):** -- pcid-spawner: computes `PCI_QUIRK_FLAGS` by calling the canonical `redox-driver-sys` lookup on synthesized `PciDeviceInfo`, then passes the env var onward +- driver-manager: bind-time consumption — spawn env hints + (`REDBEAR_DRIVER_PCI_IRQ_MODE`, `REDBEAR_DRIVER_DISABLE_ACCEL`, + `REDBEAR_DRIVER_IOMMU_GROUP`, `REDBEAR_DRIVER_NUMA_NODE`, + `REDBEAR_DRIVER_MSIX_VECTORS`) and `NEED_FIRMWARE` probe deferral, validated in + the 2026-07-23/24 QEMU gate - linux-kpi: `pci_get_quirk_flags()` / `pci_has_quirk()` C FFI is available for C drivers and is now consumed by the Red Bear amdgpu path - redbear-info: `--quirks` reads `/etc/quirks.d/*.toml` and reports configured PCI/USB/DMI entries - lspci: shows active quirk flags per PCI device (via redox-driver-sys lookup) @@ -523,6 +569,5 @@ Those selectors are used only for pre-descriptor timing flags (`RESET_DELAY`, `H `SHORT_SET_ADDR_TIMEOUT`) where vendor/product IDs are not yet available. **Remaining infrastructure work:** -- none in the current quirks scope - -`pcid-spawner` now brokers quirks through the canonical `redox-driver-sys` lookup instead of carrying a separate in-tree PCI quirk engine. +- lifecycle phases beyond spawn (`phase = "early"|"enable"` on entries — early + pre-claim vs enable; PM phases follow the power-management roadmap) diff --git a/local/recipes/drivers/redox-driver-sys/source/src/quirks/mod.rs b/local/recipes/drivers/redox-driver-sys/source/src/quirks/mod.rs index 71557bef4f..e282159690 100644 --- a/local/recipes/drivers/redox-driver-sys/source/src/quirks/mod.rs +++ b/local/recipes/drivers/redox-driver-sys/source/src/quirks/mod.rs @@ -361,6 +361,24 @@ pub fn lookup_usb_quirks(vendor: u16, product: u16) -> UsbQuirkFlags { flags } +/// Look up driver-scoped quirk flags for a domain (`"audio"`, `"nvme"`, ...) +/// from open `[[_quirk]]` TOML tables. The registry matches and +/// carries string flags; the owning driver interprets them. This is the +/// Linux Type-C model (driver-owned fixup tables such as HDA's per-codec +/// fixup lists) — the table name is the domain key, so new driver domains +/// need no registry code. +pub fn lookup_driver_quirks(domain: &str, vendor: u16, device: u16) -> Vec { + toml_loader::load_domain_quirks(domain, vendor, device).unwrap_or_default() +} + +/// Audio (HDA) driver quirk flags for a PCI vendor/device pair, from +/// `[[audio_quirk]]` entries (`quirks.d/15-audio.toml`). Mirrors Linux's +/// `sound/pci/hda` per-device quirk model: force-EAPD, single-command mode, +/// position-fix-LPIB and friends are interpreted by the audio driver itself. +pub fn lookup_audio_quirks(vendor: u16, device: u16) -> Vec { + lookup_driver_quirks("audio", vendor, device) +} + #[cfg(test)] mod tests { use super::*; diff --git a/local/recipes/drivers/redox-driver-sys/source/src/quirks/toml_loader.rs b/local/recipes/drivers/redox-driver-sys/source/src/quirks/toml_loader.rs index 86955641a8..7fd35521a2 100644 --- a/local/recipes/drivers/redox-driver-sys/source/src/quirks/toml_loader.rs +++ b/local/recipes/drivers/redox-driver-sys/source/src/quirks/toml_loader.rs @@ -61,6 +61,71 @@ pub fn load_dmi_system_quirks(dmi_info: &DmiInfo) -> Result_quirk]]` TOML table. +/// +/// Domain tables carry plain string flags; the owning driver interprets +/// them. This is the Linux Type-C model (driver-owned fixup tables such as +/// HDA's per-codec fixup lists): the registry matches and carries the data, +/// the driver is the sole consumer and semantic owner. New driver domains +/// need no registry code — the table name IS the domain key. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DomainQuirkEntry { + pub vendor: u16, + pub device: u16, + pub flags: Vec, +} + +/// Accumulate string flags from all `[[_quirk]]` entries matching +/// `(vendor, device)` (`0xFFFF` wildcards match everything). +pub fn load_domain_quirks(domain: &str, vendor: u16, device: u16) -> Result, ()> { + load_domain_quirks_from(QUIRKS_DIR, domain, vendor, device).map_err(|_| ()) +} + +fn load_domain_quirks_from( + dir: &str, + domain: &str, + vendor: u16, + device: u16, +) -> std::io::Result> { + let table_name = format!("{domain}_quirk"); + let mut out = Vec::new(); + for path in sorted_toml_files(dir)? { + let path_str = path.display().to_string(); + let Ok(content) = std::fs::read_to_string(&path) else { + continue; + }; + let Ok(doc) = content.parse::() else { + continue; + }; + let Some(arr) = doc.get(&table_name).and_then(|v| v.as_array()) else { + continue; + }; + for item in arr { + let Some(table) = item.as_table() else { + continue; + }; + let entry_vendor = table + .get("vendor") + .and_then(|v| bounded_u16(v, "vendor", &path_str)) + .unwrap_or(PCI_QUIRK_ANY_ID); + let entry_device = table + .get("device") + .and_then(|v| bounded_u16(v, "device", &path_str)) + .unwrap_or(PCI_QUIRK_ANY_ID); + let vendor_matches = entry_vendor == vendor || entry_vendor == PCI_QUIRK_ANY_ID; + let device_matches = entry_device == device || entry_device == PCI_QUIRK_ANY_ID; + if vendor_matches && device_matches { + if let Some(names) = table.get("flags").and_then(|v| v.as_array()) { + for name in names.iter().filter_map(toml::Value::as_str) { + out.push(name.to_string()); + } + } + } + } + } + Ok(out) +} + fn bounded_u16(val: &toml::Value, field: &str, path: &str) -> Option { match val.as_integer() { Some(v) => u16::try_from(v).ok().or_else(|| { @@ -703,4 +768,50 @@ mod tests { assert_eq!(rules[0].flags, PciQuirkFlags::NO_ASPM); assert_eq!(system_rules[0].flags, SystemQuirkFlags::KBD_DEACTIVATE_FIXUP); } + + #[test] + fn domain_quirks_match_and_accumulate() { + let dir = std::env::temp_dir().join(format!( + "rb-quirks-domain-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write( + dir.join("15-audio.toml"), + r#" +[[audio_quirk]] +vendor = 0x8086 +device = 0x284b +flags = ["audio_force_eapd"] + +[[audio_quirk]] +vendor = 0x8086 +flags = ["audio_single_cmd"] + +[[audio_quirk]] +vendor = 0x1002 +device = 0x4370 +flags = ["audio_position_fix_lpib"] +"#, + ) + .unwrap(); + + let exact = load_domain_quirks_from(dir.to_str().unwrap(), "audio", 0x8086, 0x284b).unwrap(); + assert_eq!(exact, vec!["audio_force_eapd", "audio_single_cmd"]); + + let other_device = load_domain_quirks_from(dir.to_str().unwrap(), "audio", 0x8086, 0x9999).unwrap(); + assert_eq!(other_device, vec!["audio_single_cmd"]); + + let other_vendor = load_domain_quirks_from(dir.to_str().unwrap(), "audio", 0x1002, 0x284b).unwrap(); + assert!(other_vendor.is_empty()); + + let wrong_domain = load_domain_quirks_from(dir.to_str().unwrap(), "nvme", 0x8086, 0x284b).unwrap(); + assert!(wrong_domain.is_empty()); + + let _ = std::fs::remove_dir_all(&dir); + } } diff --git a/local/recipes/system/redbear-quirks/source/quirks.d/15-audio.toml b/local/recipes/system/redbear-quirks/source/quirks.d/15-audio.toml index 0b999755d8..65de95d5c5 100644 --- a/local/recipes/system/redbear-quirks/source/quirks.d/15-audio.toml +++ b/local/recipes/system/redbear-quirks/source/quirks.d/15-audio.toml @@ -2,43 +2,43 @@ # These apply to HDA controllers and codec devices. # Intel ICH8 HDA — force EAPD on outputs -[[pci_quirk]] +[[audio_quirk]] vendor = 0x8086 device = 0x284b flags = ["audio_force_eapd"] # Intel ICH9 HDA (QEMU) — use immediate command interface -[[pci_quirk]] +[[audio_quirk]] vendor = 0x8086 device = 0x293e flags = ["audio_single_cmd"] # Intel 6-series PCH HDA — position fix LPIB -[[pci_quirk]] +[[audio_quirk]] vendor = 0x8086 device = 0x1c20 flags = ["audio_position_fix_lpib"] # Intel 7-series PCH HDA — position fix LPIB -[[pci_quirk]] +[[audio_quirk]] vendor = 0x8086 device = 0x1e20 flags = ["audio_position_fix_lpib"] # Intel Sunrise Point HDA — position fix LPIB -[[pci_quirk]] +[[audio_quirk]] vendor = 0x8086 device = 0xa170 flags = ["audio_position_fix_lpib"] # Intel Cannon Point HDA — position fix LPIB -[[pci_quirk]] +[[audio_quirk]] vendor = 0x8086 device = 0x9dc8 flags = ["audio_position_fix_lpib"] # AMD FCH HDA — single command fallback -[[pci_quirk]] +[[audio_quirk]] vendor = 0x1022 device = 0x1457 flags = ["audio_single_cmd"]