From 08bea46575c8cd918d468683cb44afad82a3b097 Mon Sep 17 00:00:00 2001 From: Vasilito Date: Fri, 24 Apr 2026 20:25:00 +0100 Subject: [PATCH] Fix boot-to-login: override pcid-spawner to oneshot_async, add U3 input producers, Intel HDA phases A-D - Override 00_pcid-spawner.service to oneshot_async in redbear-legacy-base.toml: rootfs phase no longer blocks on PCI driver init; getty/login starts immediately. Confirmed working on both QEMU and bare metal (redbear-live-mini). - Clean up 00_base legacy script: remove dead notify ipcd/ptyd calls, keep sudo --daemon. - Add U3 named input producers: inputd supports per-device named producers with fan-out to both device-specific consumers and legacy VT consumers. Migrate ps2d and usbhidd to InputProducer trait. RESERVED_DEVICE_NAMES deduplicated. - Add Intel HDA audio driver phases A-D: ihdad error handling (37 fixes), audio quirks, codec path enumeration, mixer/volume control. - Add init service start/readiness logging (always visible, not debug-gated). - Update BOOT-PROCESS-ASSESSMENT.md: Phase 6 complete, boot procedure documented, validation matrix updated with confirmed boot status. - Update USB-IMPLEMENTATION-PLAN.md and INPUT-SCHEME-ENHANCEMENT.md for U2/U3 status. --- config/redbear-legacy-base.toml | 38 +- local/docs/BOOT-PROCESS-ASSESSMENT.md | 220 +++- local/docs/INPUT-SCHEME-ENHANCEMENT.md | 58 +- local/docs/INTEL-HDA-IMPLEMENTATION-PLAN.md | 417 ++++++ local/docs/USB-IMPLEMENTATION-PLAN.md | 50 +- local/patches/base/redox.patch | 1114 +++++++++++++++-- .../source/quirks.d/15-audio.toml | 44 + 7 files changed, 1803 insertions(+), 138 deletions(-) create mode 100644 local/docs/INTEL-HDA-IMPLEMENTATION-PLAN.md create mode 100644 local/recipes/system/redbear-quirks/source/quirks.d/15-audio.toml diff --git a/config/redbear-legacy-base.toml b/config/redbear-legacy-base.toml index c46eda7e..0c604f5e 100644 --- a/config/redbear-legacy-base.toml +++ b/config/redbear-legacy-base.toml @@ -1,8 +1,31 @@ -# Red Bear OS overrides for broken legacy base init scripts. +# Red Bear OS overrides for legacy base init scripts. +# +# 00_base: tmpdir setup + sudo --daemon. ipcd and ptyd are started by the +# systemd-style services in base recipe's init.d/ (00_ipcd.service, +# 00_ptyd.service). The old "notify" calls have been removed because +# the "notify" binary does not exist — they always failed silently. +# sudo --daemon is kept here because 00_sudo.service exists in the base +# recipe but is not wired into any target that gets scheduled. +# 00_drivers: removed — pcid-spawner is started by 00_pcid-spawner.service from +# the base recipe. The legacy script was redundant. +# 10_net: blanked — replaced by per-config network services (e.g. redbear-live-mini's +# 10_smolnetd.service + 10_dhcpd.service). +# 00_pcid-spawner.service: overridden to oneshot_async. The base recipe uses +# type="oneshot" which blocks init until pcid-spawner exits. On real +# hardware (and QEMU), pcid-spawner can hang waiting for a PCI device +# driver that never responds, blocking the entire rootfs phase including +# getty/login. Using oneshot_async lets init proceed to start console +# services while drivers spawn in the background. [[files]] path = "/usr/lib/init.d/00_base" -data = "" +data = """ +# clear and recreate tmpdir with 0o1777 permission +rm -rf /tmp +mkdir -m a=rwxt /tmp + +nowait sudo --daemon +""" [[files]] path = "/usr/lib/init.d/00_drivers" @@ -23,3 +46,14 @@ default_dependencies = false cmd = "audiod" type = "oneshot_async" """ + +[[files]] +path = "/etc/init.d/00_pcid-spawner.service" +data = """ +[unit] +description = "PCI driver spawner (non-blocking)" + +[service] +cmd = "pcid-spawner" +type = "oneshot_async" +""" diff --git a/local/docs/BOOT-PROCESS-ASSESSMENT.md b/local/docs/BOOT-PROCESS-ASSESSMENT.md index 23b9300c..708b008c 100644 --- a/local/docs/BOOT-PROCESS-ASSESSMENT.md +++ b/local/docs/BOOT-PROCESS-ASSESSMENT.md @@ -1,8 +1,8 @@ # Red Bear OS Boot Process Assessment & Improvement Plan **Generated:** 2026-04-23 -**Updated:** 2026-04-23 -**Status:** Phase 1 ✅, Phase 2 ✅, Phase 3 ✅, Phase 4 ✅ (docs + known gaps), Phase 5 ✅ +**Updated:** 2026-04-24 +**Status:** Phase 1 ✅, Phase 2 ✅, Phase 3 ✅, Phase 4 ✅ (docs + known gaps), Phase 5 ✅, Phase 6 ✅ (boot to login confirmed) **Scope:** Comprehensive assessment of boot completeness, mistakes, robustness, resilience, and quality ## Boot Chain Overview @@ -128,12 +128,12 @@ Bare-metal testing requires physical hardware. Current validation is: ## Phase 5: Validation Matrix ✅ ### Build Verification -| Target | Build | QEMU Boot | Notes | -|--------|-------|-----------|-------| -| redbear-minimal | ✅ harddrive.img (2 GB) | ✅ Stage 2 (kernel loaded) | Login renders to framebuffer, not serial | -| redbear-full | ✅ harddrive.img (4 GB) | ✅ (prior session) | Greeter services load | -| redbear-live-mini | ✅ ISO (384 MB) | — | ISO for bare-metal boot | -| redbear-live | ✅ ISO (3.0 GB) | — | ISO for bare-metal boot | +| Target | Build | QEMU Boot | Bare-Metal Boot | Notes | +|--------|-------|-----------|-----------------|-------| +| redbear-mini | ✅ harddrive.img (2 GB) | ✅ Login prompt | — | Framebuffer console login | +| redbear-full | ✅ harddrive.img (4 GB) | ✅ Login prompt | — | Desktop packages included | +| redbear-live-mini | ✅ ISO (384 MB) | — | ✅ Login prompt | ISO for bare-metal boot | +| redbear-live-full | ✅ ISO (3.0 GB) | — | — | ISO for bare-metal boot | ### Compilation Verification - `cargo check --workspace` in base source: **0 errors** @@ -179,6 +179,82 @@ CI=1 make all CONFIG_NAME=redbear-minimal ARCH=x86_64 ## Key Technical Findings +### Bare-Metal Boot Log Analysis (2026-04-24) + +AMD machine boot log shows initfs phase starts but never completes: +- Kernel boots: ACPI, IOAPIC, timer, memory all OK +- vesad initializes: 1280x1024 at 0xA0000000 (FRAMEBUFFER_* from UEFI bootloader) +- fbbootlogd maps display +- ps2d: keyboard works, mouse BAT fails (no PS/2 mouse port — expected on modern hardware) +- pcid begins PCI enumeration +- acpid starts, AML interpreter initializes +- **MISSING**: "init: initfs drivers target step() complete" — scheduler.step() never returns +- **MISSING**: "init: phase 2 — switchroot to /usr" — rootfs phase never starts +- **MISSING**: any getty or login output + +Root cause hypothesis (unproven): a service with `type = "notify"`, `type = { scheme = "..." }`, +or `type = "oneshot"` in the initfs phase does not signal readiness or does not exit, +causing init's scheduler.step() to block forever. All three service types wait synchronously +in `service.rs`. Possible blockers include: +- A `notify` service that hangs before calling `daemon::Daemon::ready()` +- A `scheme` service that hangs before calling `daemon::SchemeDaemon::ready_*()` +- An `oneshot` service like `pcid-spawner --initfs` that hangs during PCI enumeration +With the new per-service logging (Phase 6A + 6C), the next boot will show exactly which +service blocks — the last `init: starting ...` line before the hang identifies the blocker. + +### Bare-Metal/QEMU Boot Log Analysis (2026-04-24, second test with Phase 6 logging) + +The enhanced logging proved the initfs phase completes successfully. The actual blocker is +in the rootfs phase: + +- Initfs phase: ✅ all services start and signal readiness/exit correctly +- `init: phase 2 - switchroot to /usr` ✅ +- `init: scheduling 22 rootfs units` ✅ +- `init: starting PCI driver spawner (pcid-spawner)` ← **BLOCKS HERE** +- pcid-spawner (rootfs, `type = "oneshot"`) spawns e1000d (ok), ihdad (fails with RIRB timeout) +- Then hangs — no further output for 30+ seconds while system is alive (keyboard works) +- Init never reaches `30_console` → getty → login + +Root cause (confirmed): rootfs `00_pcid-spawner.service` uses `type = "oneshot"`, which +causes init to block until pcid-spawner exits. On real hardware and QEMU, pcid-spawner +can hang waiting for a PCI device driver that never responds, blocking the entire rootfs +phase including getty/login. + +Fix: override `00_pcid-spawner.service` to `type = "oneshot_async"` in +`config/redbear-legacy-base.toml`. Drivers spawn in the background while init proceeds +to start console services. Network services that depend on specific drivers handle their +own timing (they connect to driver schemes when ready). + +**Confirmed working**: Both QEMU and bare-metal boot to login prompt after this fix. + +### Phase 6: Boot Visibility & Service Cleanup ✅ + +**Status: Confirmed working — system boots to login prompt on both QEMU and bare metal.** + +**6A: Init service start logging (always visible)** +`init/src/scheduler.rs`: Service and target start messages promoted from DEBUG to always-visible. +Every service now logs `init: starting ()` before spawning and +`init: started (pid )` after a respawnable process is created. + +**6B: Legacy init script cleanup** +`config/redbear-legacy-base.toml`: +- `00_base`: Removed dead `notify ipcd` / `notify ptyd` calls. + The `notify` binary does not exist anywhere in the build tree — these calls always failed + silently. ipcd and ptyd are started by the base recipe's systemd-style services + (`00_ipcd.service`, `00_ptyd.service`). sudo --daemon is kept because `00_sudo.service` + exists in the base recipe but is not wired into any target that gets scheduled. + The script now does tmpdir setup + sudo --daemon. +- `00_drivers`: Blanked (was redundant — pcid-spawner starts via `00_pcid-spawner.service`). + +**6C: Service readiness completion logging** +`init/src/service.rs`: Added success log after each blocking wait completes: +- `notify` services: `init: ready (notify)` after readiness byte received +- `scheme` services: `init: ready (scheme )` after scheme registered +- `oneshot` services: `init: done (oneshot)` after process exits successfully +Combined with 6A's `init: starting ...` before spawn, the boot log now shows the full +lifecycle of every blocking service — any gap between "starting" and "ready/done" pinpoints +the blocker. + ### Serde `deny_unknown_fields` Behavior `UnitInfo` and `Service` structs use `#[serde(deny_unknown_fields)]`. Any unrecognized field in `[unit]` or `[service]` sections causes the ENTIRE service file to fail deserialization. The init system logs the error and skips the service — it never starts. @@ -192,10 +268,18 @@ Services with `type = "oneshot_async"` are fire-and-forget by default. Init spaw ### Config Include Chain ``` -redbear-minimal.toml → minimal.toml, redbear-legacy-base.toml, redbear-device-services.toml, redbear-netctl.toml -redbear-full.toml → desktop.toml, redbear-desktop.toml, redbear-greeter-services.toml, ... +redbear-live-full.toml → redbear-live.toml +redbear-live.toml → redbear-full.toml +redbear-full.toml → desktop.toml, redbear-legacy-base.toml, redbear-legacy-desktop.toml, + redbear-device-services.toml, redbear-netctl.toml, redbear-greeter-services.toml +desktop.toml → desktop-minimal.toml, server.toml +desktop-minimal.toml → minimal.toml +server.toml → minimal.toml +minimal.toml → base.toml + redbear-live-mini.toml → minimal.toml, redbear-legacy-base.toml, redbear-netctl.toml -redbear-live.toml → redbear-full.toml, ... +redbear-mini → redbear-minimal.toml → minimal.toml, redbear-legacy-base.toml, + redbear-device-services.toml, redbear-netctl.toml ``` ### Upstream Targets (not Red Bear defined) @@ -266,3 +350,117 @@ redbear-live.toml → redbear-full.toml, ... - `local/scripts/validate-service-files.sh` — manual service schema validation (redbear-*.toml only) - `local/docs/BOOT-PROCESS-ASSESSMENT.md` — this document - `recipes/core/base/source/init.initfs.d/41_acpid.service` — acpid in initfs (boot race fix) + +## Boot Procedure + +### Supported compile targets + +| Target | Purpose | Output | +|--------|---------|--------| +| `redbear-mini` | Minimal non-desktop (QEMU + bare metal) | `build/x86_64/harddrive.img` | +| `redbear-live-mini` | Minimal live ISO (bare metal only) | `build/x86_64/redbear-live-mini.iso` | +| `redbear-full` | Desktop/graphics (QEMU + bare metal) | `build/x86_64/harddrive.img` | +| `redbear-live-full` / `redbear-live` | Desktop/graphics live ISO (bare metal only) | `build/x86_64/redbear-live-full.iso` | + +### Build commands + +```bash +# Minimal target (QEMU testing) +CI=1 make all CONFIG_NAME=redbear-mini ARCH=x86_64 + +# Minimal live ISO (bare-metal boot) +CI=1 make live CONFIG_NAME=redbear-live-mini ARCH=x86_64 + +# Desktop/graphics target (QEMU testing) +CI=1 make all CONFIG_NAME=redbear-full ARCH=x86_64 + +# Desktop/graphics live ISO (bare-metal boot) +CI=1 make live CONFIG_NAME=redbear-live-full ARCH=x86_64 +``` + +### QEMU boot (harddrive.img) + +```bash +# Boot the minimal target in QEMU +make qemu CONFIG_NAME=redbear-mini + +# Boot with more RAM +make qemu CONFIG_NAME=redbear-mini QEMUFLAGS="-m 4G" + +# Boot desktop target +make qemu CONFIG_NAME=redbear-full +``` + +QEMU boots from `harddrive.img` (not the live ISO). The `-serial mon:stdio` flag provides +the serial console, but Red Bear uses the framebuffer console for login — type at the +graphical console, not serial. + +### Bare-metal boot (live ISO) + +1. **Build the ISO:** + ```bash + CI=1 make live CONFIG_NAME=redbear-live-mini ARCH=x86_64 + ``` + +2. **Write ISO to USB drive:** + ```bash + sudo dd if=build/x86_64/redbear-live-mini.iso of=/dev/sdX bs=4M status=progress && sync + ``` + Replace `/dev/sdX` with your USB device. Use `lsblk` to identify it. + +3. **Boot from USB:** + - Insert USB into target machine + - Power on, enter UEFI boot menu (typically F12, F8, or Esc) + - Select the USB device as boot target + - Red Bear OS boots from UEFI → bootloader → kernel → init → login prompt + +4. **Login:** + - Default user: `root`, no password + - The framebuffer console displays the login prompt after boot completes + +### What happens during boot + +``` +UEFI firmware + → Red Bear bootloader (loaded from EFI system partition) + → Kernel (kstart → start → kmain) + → userspace_init → bootstrap (forks initfs/procmgr/initnsmgr) + → Initfs phase: + logd, inputd, vesad (framebuffer), fbcond, fbbootlogd, + ps2d (keyboard), acpid, pcid-spawner-initfs (initfs PCI drivers), lived, redoxfs + → switchroot /usr + → Rootfs phase: + 00_base (tmpdir + sudo --daemon) + 00_ipcd.service, 00_ptyd.service + 00_pcid-spawner.service (async — spawns PCI drivers in background) + 30_console (getty with respawn) + → Login prompt on framebuffer console +``` + +### Boot log markers + +The init system logs the following always-visible markers. If boot hangs, the last visible +marker identifies the blocker: + +``` +init: phase 1 — initfs boot +init: starting () # before each service spawn +init: ready (notify) # notify-type service ready +init: ready (scheme ) # scheme-type service ready +init: done (oneshot) # oneshot service exited +init: phase 2 — switchroot to /usr +init: scheduling N rootfs units +init: reached target +init: phase 3 — rootfs services started +init: boot complete — entering waitpid loop +``` + +### Troubleshooting + +| Symptom | Likely cause | Fix | +|---------|-------------|-----| +| No display output | UEFI framebuffer not provided | Try different USB port or disable CSM in UEFI settings | +| Boot hangs after "scheduling N rootfs units" | A blocking service hangs | Check last "starting" line; `pcid-spawner` was previously the blocker | +| Keyboard not working | PS/2 unavailable, USB not ready | Modern hardware uses USB — ensure xHCI controller is functional | +| No login prompt | Getty not starting | Check `30_console` service in config; verify getty respawn is set | +| "missing field `unit`" parse error | Invalid service TOML | Run `./local/scripts/validate-service-files.sh config/` | diff --git a/local/docs/INPUT-SCHEME-ENHANCEMENT.md b/local/docs/INPUT-SCHEME-ENHANCEMENT.md index 87e4e37e..b007499f 100644 --- a/local/docs/INPUT-SCHEME-ENHANCEMENT.md +++ b/local/docs/INPUT-SCHEME-ENHANCEMENT.md @@ -26,11 +26,11 @@ The current `inputd` implementation in `recipes/core/base/source/drivers/inputd/ - `SchemeRoot` exists, but it is not a real directory yet: it does not enumerate entries. - `lib.rs` only exposes `ProducerHandle`, `ConsumerHandle`, `DisplayHandle`, and `ControlHandle`. -Current callers confirm the limitation: +Current callers after migration: -- `ps2d` opens one `ProducerHandle` and sends both keyboard and mouse events into the same stream. -- `usbhidd` also opens one `ProducerHandle` and sends keyboard/mouse/button/scroll data into the same stream. -- local `evdevd` reads `/scheme/input/consumer`, receives anonymous mixed `orbclient::Event` values, and manually translates them. +- `ps2d` opens two `InputProducer` instances (`ps2-keyboard`, `ps2-mouse`) with legacy fallback, routing keyboard scancodes to the keyboard producer and mouse events to the mouse producer. +- `usbhidd` opens one `InputProducer` per interface instance (`usb-{port}-if{n}`) with legacy fallback. +- local `evdevd` reads `/scheme/input/consumer`, receives anonymous mixed `orbclient::Event` values, and manually translates them (not yet migrated to per-device streams). ## 3. Design Principles @@ -454,23 +454,20 @@ This keeps `DeviceConsumer` simple and avoids introducing a second handle teardo ## 13. Migration Path -### 13.1 `ps2d` +### 13.1 `ps2d` — MIGRATED -`ps2d` is the first caller that should adopt the new API because it already has a clean split between keyboard and mouse sources. +`ps2d` now uses two `InputProducer` instances with named-first, legacy-fallback strategy: -Recommended startup logic: - -1. Try `NamedProducerHandle::new("ps2-keyboard")` -2. Try `NamedProducerHandle::new("ps2-mouse")` -3. If both succeed, run in named mode -4. If either fails, close any partially opened named handle and fall back to one legacy `ProducerHandle::new()` +1. Try `InputProducer::new_named_or_fallback("ps2-keyboard")` → falls back to legacy on error +2. Try `InputProducer::new_named_or_fallback("ps2-mouse")` → falls back to legacy on error +3. `Ps2d` struct holds `keyboard_input: InputProducer` + `mouse_input: InputProducer` Routing: -- keyboard scancodes → `ps2-keyboard` -- mouse move / absolute move / button / scroll events → `ps2-mouse` +- keyboard scancodes → `self.keyboard_input` +- mouse move / absolute move / button / scroll events → `self.mouse_input` -This preserves compatibility with old `inputd` while immediately enabling per-device consumers on new `inputd`. +This preserves compatibility with old `inputd` while enabling per-device consumers on new `inputd`. ### 13.2 `evdevd` @@ -482,9 +479,15 @@ Once the scheme exists, local `evdevd` can move from `/scheme/input/consumer` to It can keep the legacy consumer path as a fallback for older systems. -### 13.3 `usbhidd` +### 13.3 `usbhidd` — MIGRATED -`usbhidd` can remain legacy initially, then later migrate to named producers such as `usb-hid0`, `usb-hid1`, or more specific per-interface names. +`usbhidd` now uses one `InputProducer` per interface instance with named-first, legacy-fallback strategy: + +1. Opens `InputProducer::new_named_or_fallback(&format!("usb-{}-if{}", port, interface_num))` +2. Falls back to legacy on error +3. All event writes go through the same `write_event()` method + +Producer names: `usb-{port}-if{interface_num}` (e.g., `usb-1-if0`, `usb-1-if1`). ## 14. Backward Compatibility Requirements @@ -520,16 +523,17 @@ This design does **not** include: Another developer implementing this design should be able to proceed in this order: -1. extend `Handle` and `InputScheme` state -2. teach `openat()` to parse `producer/{name}`, `events`, and dynamic device names -3. add root `getdents()` support for `SchemeRoot` -4. refactor `write()` so producer type is detected before routing -5. fan out named-producer events to matching `DeviceConsumer` handles and the existing legacy path -6. add hotplug queue serialization helpers -7. extend `fevent()` and daemon notification loop for `DeviceConsumer` and `HotplugEvents` -8. add cleanup in `on_close()` for `NamedProducer` -9. extend `lib.rs` with the new handle types and directory lister -10. migrate `ps2d` with a named-producer-first, legacy-fallback strategy +1. extend `Handle` and `InputScheme` state ✅ +2. teach `openat()` to parse `producer/{name}`, `events`, and dynamic device names ✅ +3. add root `getdents()` support for `SchemeRoot` ✅ +4. refactor `write()` so producer type is detected before routing ✅ +5. fan out named-producer events to matching `DeviceConsumer` handles and the existing legacy path ✅ +6. add hotplug queue serialization helpers ✅ +7. extend `fevent()` and daemon notification loop for `DeviceConsumer` and `HotplugEvents` ✅ +8. add cleanup in `on_close()` for `NamedProducer` ✅ +9. extend `lib.rs` with the new handle types and directory lister ✅ +10. migrate `ps2d` with a named-producer-first, legacy-fallback strategy ✅ +11. migrate `usbhidd` with a named-producer-first, legacy-fallback strategy ✅ ## 17. Final Outcome diff --git a/local/docs/INTEL-HDA-IMPLEMENTATION-PLAN.md b/local/docs/INTEL-HDA-IMPLEMENTATION-PLAN.md new file mode 100644 index 00000000..145339f9 --- /dev/null +++ b/local/docs/INTEL-HDA-IMPLEMENTATION-PLAN.md @@ -0,0 +1,417 @@ +# Intel HDA Implementation Plan + +**Version:** 1.0 (2026-04-24) +**Status:** Draft execution plan +**Scope owner:** Audio subsystem (legacy HDA + Intel DSP decision path) + +## Purpose + +This document defines the concrete execution plan for implementing full Intel audio support in Red Bear OS, using Linux 7.0 source code in-tree as donor reference material. + +"Full Intel support" is split into three tracks: + +1. Legacy PCI HDA controller + analog codecs (current `ihdad` path) +2. HDMI/DP digital audio over HDA links +3. Modern Intel DSP-class platforms (SOF/AVS-class routing, not legacy-only HDA) + +## Why This Plan Is Needed + +Current in-tree evidence shows `ihdad` is an early implementation, not a complete Intel audio stack: + +- Single-codec assumption in enumeration logic (`device.rs`) +- Unimplemented controller interrupt handler (`handle_controller_interrupt`) +- Fixed-format playback setup (44.1kHz / 16-bit / stereo) +- Incomplete scheme surface (`Handle::Todo`-centric behavior) +- No complete capture path integration in `audiod` (`TODO: audio input`) +- Historical hardware report: "No audio, HDA driver cannot find output pins" + +## Current Stack Snapshot + +### Driver and daemon surface + +- `ihdad` registers `audiohw` +- `audiod` opens `/scheme/audiohw` and exposes `/scheme/audio` +- SDL backends use `/scheme/audio` + +### Known contract constraints + +- `audiod` mixes fixed-size buffers (`HW_BUFFER_SIZE = 512`) +- `ihdad` stream writes currently assume strict block sizing +- `ihdad` currently hardcodes one primary output format on setup + +## Canonical Donor Sources (Linux 7.0 in-tree) + +- Controller policy and quirks: + - `build/linux-kernel-cache/linux-7.0/sound/hda/controllers/intel.c` +- Generic parser and fixup engine: + - `build/linux-kernel-cache/linux-7.0/sound/hda/common/auto_parser.c` +- Core codec/controller plumbing: + - `build/linux-kernel-cache/linux-7.0/sound/hda/common/` +- Vendor codec implementations: + - `build/linux-kernel-cache/linux-7.0/sound/hda/codecs/` +- Intel DSP route-selection policy: + - `build/linux-kernel-cache/linux-7.0/sound/hda/core/intel-dsp-config.c` +- Modern Intel DSP implementations: + - `build/linux-kernel-cache/linux-7.0/sound/soc/sof/intel/` + - `build/linux-kernel-cache/linux-7.0/sound/soc/intel/avs/` + +## Execution Model + +The plan is organized as issue-sized work packages (`HDA-001`..`HDA-012`). + +### Phase A: Legacy HDA correctness (must complete first) + +#### HDA-001 — Multi-codec and function-group support + +**Goal:** Remove single-codec assumptions and support real codec topology. + +**Files:** +- `recipes/core/base/source/drivers/audio/ihdad/src/hda/device.rs` +- `recipes/core/base/source/drivers/audio/ihdad/src/hda/node.rs` + +**Acceptance criteria:** +- Codec enumeration includes all detected codecs +- Bring-up does not assume first codec is the audio path +- `audiohw:codec` dump reflects multi-codec topology + +#### HDA-002 — Controller interrupts and unsolicited events + +**Goal:** Implement real controller interrupt handling and unsol event dispatch. + +**Files:** +- `recipes/core/base/source/drivers/audio/ihdad/src/hda/device.rs` +- `recipes/core/base/source/drivers/audio/ihdad/src/hda/cmdbuff.rs` + +**Acceptance criteria:** +- `handle_controller_interrupt()` is non-stub +- Jack-related unsol events are observable and processed +- No interrupt-ack regressions under continuous playback + +#### HDA-003 — Format/rate/channel negotiation + +**Goal:** Replace fixed-format startup with negotiated stream format. + +**Files:** +- `recipes/core/base/source/drivers/audio/ihdad/src/hda/device.rs` +- `recipes/core/base/source/drivers/audio/ihdad/src/hda/stream.rs` + +**Acceptance criteria:** +- Driver selects supported stream format from capabilities +- Unsupported format requests fail deterministically +- Startup no longer assumes 44.1kHz/16-bit/stereo only + +#### HDA-004 — Real scheme endpoint model (`pcmout`/`pcmin`) + +**Goal:** Replace `Handle::Todo` behavior with structured stream handles. + +**Files:** +- `recipes/core/base/source/drivers/audio/ihdad/src/hda/device.rs` +- `recipes/core/base/source/audiod/src/scheme.rs` + +**Acceptance criteria:** +- Distinct playback and capture endpoints exist +- Handle lifecycle and permissions are explicit +- Multiple clients can be supported without implicit index-0 fallback + +#### HDA-005 — Capture and duplex path + +**Goal:** Implement and validate simultaneous input/output. + +**Files:** +- `recipes/core/base/source/drivers/audio/ihdad/src/hda/device.rs` +- `recipes/core/base/source/drivers/audio/ihdad/src/hda/stream.rs` +- `recipes/core/base/source/audiod/src/main.rs` +- `recipes/core/base/source/audiod/src/scheme.rs` + +**Acceptance criteria:** +- Capture endpoint is functional +- Duplex playback/capture runs stably for bounded runtime tests +- `audiod` input TODO is removed + +### Phase B: Parser, fixups, and quirk-driven stability + +#### HDA-006 — Generic parser + fixup framework + +**Goal:** Add parser/fixup framework equivalent to Linux generic HDA model. + +**Files:** +- New parser/fixup module(s) under `ihdad/src/hda/` +- Integration in `device.rs` + +**Acceptance criteria:** +- Pin/path selection is parser-driven, not heuristic-only +- Fixups can be applied by device identity and pin/config criteria +- Targeted fixup can resolve known "no output pins" class failures + +#### HDA-007 — Audio quirk data pipeline + +**Goal:** Add audio quirk extraction and runtime loading pattern aligned with current quirks system. + +**Files:** +- `local/scripts/extract-linux-quirks.py` (extend for HDA tables) +- `local/recipes/drivers/redox-driver-sys/source/src/quirks/mod.rs` (add audio quirk model) +- `local/recipes/system/redbear-quirks/source/quirks.d/` (add audio quirk TOML) + +**Acceptance criteria:** +- Audio quirk entries load from `/etc/quirks.d` +- Driver behavior can be changed by data without code edits +- At least MSI/probe/position/power policy classes represented + +#### HDA-008 — Controller policy parity slice + +**Goal:** Add minimum policy knobs parity with Linux HDA controller behavior. + +**Files:** +- `recipes/core/base/source/drivers/audio/ihdad/src/hda/device.rs` +- `recipes/core/base/source/drivers/audio/ihdad/src/main.rs` + +**Initial parity targets:** +- MSI policy +- single-command fallback policy +- codec probe mask +- DMA position-fix policy +- jack poll fallback policy + +**Acceptance criteria:** +- Policies are configurable and observable +- Policy defaults can be influenced by quirk data + +### Phase C: Digital audio completeness + +#### HDA-009 — HDMI/DP audio path + +**Goal:** Implement digital codec path handling including ELD and sink constraints. + +**Files:** +- New digital-audio module(s) in `ihdad/src/hda/` +- Integration points in `device.rs` + +**Acceptance criteria:** +- HDMI/DP codec path is detected and usable on supported hardware/VMs +- ELD-informed format/channel limitations are honored + +### Phase D: Modern Intel audio (DSP-class) + +#### HDA-010 — Intel audio route dispatcher + +**Goal:** Add driver-selection logic equivalent to Linux `intel-dsp-config` principles. + +**Files:** +- New dispatcher logic in audio/pcid integration path +- `recipes/core/base/source/drivers/audio/ihdad/config.toml` and related registration surfaces + +**Acceptance criteria:** +- cAVS/SOF-class devices are not incorrectly routed to legacy-only behavior +- Route decision uses bounded platform traits (PCI class/prog-if + board traits) + +#### HDA-011 — SOF/AVS-class implementation track + +**Goal:** Provide a modern Intel DSP-capable driver path separate from legacy `ihdad`. + +**Donor roots:** +- `sound/soc/sof/intel` +- `sound/soc/intel/avs` + +**Acceptance criteria:** +- At least one Intel cAVS/SOF-class machine can produce bounded playback +- Legacy HDA path remains intact on legacy devices + +### Phase E: Desktop ecosystem compatibility + +#### HDA-012 — PipeWire/PulseAudio compatibility bridge + +**Goal:** Bridge Redox native audio to desktop software expecting PipeWire/PulseAudio APIs. + +**Acceptance criteria:** +- KDE desktop audio consumers can produce sound through compatibility layer +- Scope and claim language remains bounded (no overclaim) + +## Validation Gates + +### G1 — Legacy HDA playback stability + +- Environment: QEMU HDA and at least one bare-metal Intel HDA device +- Criteria: + - Sustained playback duration threshold met + - No IRQ storm, no driver lockup + - No repeated timeout errors from CORB/RIRB paths + +### G2 — Jack event behavior + +- Environment: bare metal with physical jack +- Criteria: + - Plug/unplug detected + - Route switches correctly + - Speaker mute policy behaves as expected + +### G3 — Duplex stability + +- Environment: bare metal preferred; QEMU for baseline +- Criteria: + - Capture + playback concurrently + - No starvation/deadlock in scheme processing + +### G4 — Quirk efficacy + +- Criteria: + - At least 3 hardware-specific issues fixed by data-driven quirks + - Fixes do not require permanent ad hoc branches in main flow + +### G5 — Modern Intel path + +- Environment: Intel cAVS/SOF-class system +- Criteria: + - Route dispatcher selects modern path + - Bounded playback success via DSP-capable path + +## Risk and Dependency Notes + +1. **Main risk:** Treating SOF/AVS systems as legacy HDA-only. +2. **Main technical debt risk:** Hardcoded policy instead of quirk-backed data. +3. **Integration dependency:** `audiod` contract must evolve in lockstep with `ihdad` stream model. +4. **Desktop dependency:** KDE audio integration remains blocked without compatibility bridge even if kernel/driver path works. + +## Initial Prioritization (strict order) + +1. HDA-001 through HDA-005 +2. HDA-006 through HDA-008 +3. HDA-009 +4. HDA-010 and HDA-011 +5. HDA-012 + +## HDA-001 Implementation Blueprint + +This section defines the concrete first code slice for `HDA-001` (multi-codec + function-group support). + +### Objective + +Remove hardcoded codec `0` traversal and make codec/AFG/widget discovery data-driven from `STATESTS`. + +### Current hotspots + +- `ihdad` codec discovery currently hardcodes `let codec: u8 = 0` during enumeration. +- Widget addressing and lists (`outputs`, `inputs`, pins) are global vectors not grouped by codec/function group. +- The scheme dump path (`audiohw:codec`) assumes a single codec payload. + +### Files to edit + +- `recipes/core/base/source/drivers/audio/ihdad/src/hda/device.rs` +- `recipes/core/base/source/drivers/audio/ihdad/src/hda/node.rs` (only if helper fields/methods are needed) + +### Step-by-step patch plan + +1. Introduce per-codec topology container in `device.rs`. + + Add internal structures: + + - `CodecTopology` + - `codec_addr: CodecAddr` + - `afgs: Vec` + - `widget_map: HashMap` + - `outputs: Vec` + - `inputs: Vec` + - `output_pins: Vec` + - `input_pins: Vec` + - `beep_addr: Option` + + - `IntelHDA` field: + - `codecs_topology: HashMap` + +2. Replace global widget collections with codec-scoped accessors. + + Keep existing fields temporarily for migration safety, but make enumeration write to `codecs_topology` first. + After compile + smoke pass, remove stale globals (`outputs`, `inputs`, `widget_map`, pin vectors, `beep_addr`). + +3. Refactor `enumerate()` to iterate all detected codecs. + + - Use `self.codecs` as source (populated in `reset_controller()` from `STATESTS`). + - For each codec: + - Read root node `(codec, 0)`. + - Iterate all function groups in root range. + - Filter audio function groups (`function_group_type` audio class). + - Enumerate widgets and classify into per-codec topology lists. + +4. Add safe codec selection helper for playback bring-up. + + Add helper: + - `fn pick_primary_codec_for_output(&self) -> Option` + + Selection policy v1: + - First codec with at least one `output_pin` and one `AudioOutput` widget. + - Stable tie-breaker: lowest codec address. + +5. Make `find_best_output_pin()` codec-aware. + + Change signature from global behavior to: + - `fn find_best_output_pin(&mut self, codec: CodecAddr) -> Result` + + Ensure all widget lookups use the selected codec topology map. + +6. Update path walk helpers to consume codec-scoped maps. + + - `find_path_to_dac()` should use the selected codec topology `widget_map`. + - Avoid `.unwrap()` on map lookups in traversal; return `None`/`Err(ENODEV)` on missing nodes. + +7. Update `configure()` to use selected codec. + + - Choose codec via `pick_primary_codec_for_output()`. + - Call `find_best_output_pin(codec)`. + - Resolve DAC path only within that codec. + +8. Update codec dump endpoint to expose all codecs. + + - Keep `openat("codec")` behavior, but include per-codec sections in output. + - Optional follow-up: add `codec/` path support; not required for first slice. + +9. Guard rails for no-audio cases. + + - If codecs are present but no valid output topology found, return structured `ENODEV`. + - Do not panic on `No output pins`. + +### Non-goals for HDA-001 + +- Jack unsolicited handling (`HDA-002`) +- Capture stream enablement (`HDA-005`) +- Policy quirks (`HDA-008`) +- HDMI/DP ELD path (`HDA-009`) + +### Compile + runtime checks for this slice + +1. Build driver package: + + - `./target/release/repo cook recipes/core/base` + - or full base target flow already used by this tree + +2. Boot in QEMU with HDA enabled: + + - validate `ihdad` starts without panic + - read codec dump from `audiohw:codec` + +3. Verify acceptance: + + - Multiple codec entries are shown when available + - Single-codec machines still work + - No regression in existing playback path on QEMU ICH9 HDA + +### Exit criteria for closing HDA-001 + +- Enumeration is no longer hardcoded to codec 0. +- Playback path can choose a valid codec deterministically. +- Codec dump includes all detected codecs. +- `ihdad` no longer panics when output pins are missing on codec 0 but present on another codec. + +## Claim Language Policy + +Until G1-G5 gates are met, support claims must remain bounded: + +- Use: "builds", "enumerates", "bounded playback proof" +- Avoid: "full Intel audio support" or broad compatibility claims + +## Related Documents + +- `local/docs/LINUX-BORROWING-RUST-IMPLEMENTATION-PLAN.md` +- `local/docs/QUIRKS-SYSTEM.md` +- `local/docs/QUIRKS-IMPROVEMENT-PLAN.md` +- `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` +- `docs/05-KDE-PLASMA-ON-REDOX.md` +- `recipes/core/base/source/drivers/COMMUNITY-HW.md` diff --git a/local/docs/USB-IMPLEMENTATION-PLAN.md b/local/docs/USB-IMPLEMENTATION-PLAN.md index 89efe24d..69439671 100644 --- a/local/docs/USB-IMPLEMENTATION-PLAN.md +++ b/local/docs/USB-IMPLEMENTATION-PLAN.md @@ -27,7 +27,7 @@ This repo should not treat **builds** or **enumerates** as equivalent to **valid USB driver code lives in `recipes/core/base/source/drivers/usb/`, which is an upstream-managed git working copy. Red Bear carries all USB modifications through `local/patches/base/redox.patch` -(currently 5029 lines, 23 diff sections, 16 USB/HID/storage-related). +(currently ~17000 lines across ~100 diff sections). **Upstream state** — the unpatched source snapshot that `make fetch` produces — has significant error handling gaps and several correctness bugs. Red Bear's patch layer fixes these, but the fixes @@ -124,7 +124,7 @@ source: Even with the Red Bear patch applied: -- HID is still wired through the legacy mixed-stream `inputd` path +- HID is now wired through named producers (`ps2-keyboard`, `ps2-mouse`, `usb-{port}-if{n}`); named producers always fan out to both per-device consumers and the legacy VT consumer path; the `InputProducer` wrapper falls back to an anonymous legacy `ProducerHandle` if the named path is unavailable (e.g., older `inputd` build) - external USB keyboard fallback is not guaranteed on bare metal unless the keyboard reaches the xHCI runtime path - EHCI/UHCI/OHCI are not yet full runtime host-controller implementations @@ -151,11 +151,11 @@ Even with the Red Bear patch applied: | xHCI controller | **builds / QEMU-validated** | Red Bear patch: 88 error handling fixes, ERDP split, endp_direction fix, cfg_idx fix, real grow_event_ring, mutex poison recovery on all hot-path locks; no real hardware validation yet | | EHCI/UHCI/OHCI | **builds / enumerates** | Ownership, port handling, and logging exist, but they are not yet full runtime enumeration paths | | Hub handling | **builds / good quality** | `usbhubd`: all `expect()` eliminated, interrupt-driven change detection with polling fallback, graceful per-port error handling | -| HID | **builds / QEMU-validated in narrow path** | `usbhidd` handles keyboard/mouse/button/scroll via legacy input path, no panics in report loop; keyboard LED sync exists as a bounded per-device best-effort path | +| HID | **builds / QEMU-validated in narrow path** | `usbhidd` handles keyboard/mouse/button/scroll via named producer path (`usb-{port}-if{n}`) with legacy fallback, no panics in report loop; keyboard LED sync exists as a bounded per-device best-effort path | | Mass storage | **builds / good quality** | `usbscsid`: typed `ScsiError`, fallible parsing, `ReadCapacity16` for >2TB, stall recovery, resilient event loop | | Native tooling | **builds / enumerates** | `lsusb`, `usbctl`, `redbear-info`, `redbear-usb-check` provide observability | | Low-level userspace API | **builds** | `xhcid_interface` with `UsbSpeed` enum, `attach_with_speed()` | -| Validation | **builds / QEMU-only** | 3 harness scripts + in-guest checker; no real hardware validation scripts | +| Validation | **builds / QEMU-only** | 4 harness scripts + in-guest checker; no real hardware validation scripts | | Hardware quirks | **builds** | `redox-driver-sys` quirk tables with 146 compiled-in USB quirk entries (mined from Linux 7.0) + 22 USB quirk flags; runtime TOML loading for `/etc/quirks.d/` | ## Code Quality by Daemon @@ -183,7 +183,7 @@ Key files and their sizes: | Daemon | Lines | Error Handling Quality | Remaining unwrap/expect | Key Gaps | |---|---|---|---|---| | `usbhubd` | ~430 | **Good** — `Result<(), Box>`, all `expect()` eliminated, interrupt-driven change detection | 0 | 1-second polling fallback if interrupt EP unavailable | -| `usbhidd` | 576 | **Good** — `anyhow::Result` with context, zero `unwrap()`/`expect()` | 0 | Hardcoded 1ms poll rate; mouse ×2 multiplier workaround; X scroll missing | +| `usbhidd` | 576 | **Good** — `anyhow::Result` with context, no panics in report loop; `expect()` remains in arg parsing and descriptor setup (pre-existing) | 7 `expect()` + 1 `assert_eq!` (pre-existing, arg parsing/descriptor setup) | Hardcoded 1ms poll rate; mouse ×2 multiplier workaround; X scroll missing | | `usbscsid` | ~1800 | **Good** — `ScsiError` typed errors, fallible `parse_bytes`/`parse_mut_bytes` helpers, resilient event loop, `ReadCapacity16` | 0 | — | ## Validation Infrastructure @@ -301,8 +301,19 @@ hardware; controller enumerates attached devices reliably across repeated boot c **Remaining**: - Validate repeated attach/detach/reset behavior under stress (requires real hardware) -- Support non-default configurations and alternate settings (requires xHCI config logic in scheme.rs) -- Improve composite-device handling and endpoint selection across interfaces (requires xHCI config logic in scheme.rs) + +**Completed (Red Bear patch, this session)**: +- `configure_endpoints_once()` now filters endpoints by specific interface+alternate when + `req.interface_desc` is set, enabling composite-device drivers to claim individual interfaces + without programming endpoints from other interfaces +- When `interface_desc` is `None` (initial device setup), endpoints are collected from all + default-alternate (alt 0) interfaces, preserving backward compatibility +- `PortState.active_ifaces: BTreeMap` tracks which interface numbers are active and + which alternate setting each is using +- `set_interface()` now updates `active_ifaces` after a successful SET_INTERFACE control request +- `spawn_drivers()` logs non-default alternates at debug level instead of warning, documenting + that non-default alternates are selected by drivers via SET_INTERFACE rather than auto-spawn +- Initial configuration populates `active_ifaces` with all default-alternate interfaces **Where**: `recipes/core/base/source/drivers/usb/usbhubd/`, `xhcid/src/xhci/scheme.rs` @@ -316,14 +327,25 @@ least one composite device configures correctly beyond the simplest path. **Status**: Partially complete. **Completed (Red Bear patch)**: -- `usbhidd` error handling improved — `anyhow::Result` with context, no panics in report loop, zero `unwrap()`/`expect()` calls -- `assert_eq!` replaced with `anyhow::bail!` +- `usbhidd` error handling improved — `anyhow::Result` with context, no panics in report loop; `expect()`/`assert_eq!` remain in arg parsing and descriptor setup (pre-existing) - Display write failures logged as warnings instead of panicking +- `inputd` scheme enhancement: named producers (`/scheme/input/producer/{name}`), per-device + consumer streams (`/scheme/input/{device_name}`), hotplug event stream (`/scheme/input/events`), + root directory enumeration (static entries + dynamic device names) +- Named producer events fan out to both matching DeviceConsumers and the legacy VT consumer path +- Hotplug binary format: 16-byte header (kind, device_id, name_len, reserved) + UTF-8 name +- Device IDs allocated monotonically, never reused +- Public API: `NamedProducerHandle`, `DeviceConsumerHandle`, `HotplugHandle`, `InputDeviceLister`, + `InputProducer` (named-first, legacy-fallback convenience wrapper) +- All legacy paths, event payloads, VT behavior, and display/control behavior preserved unchanged +- `ps2d` migrated: two `InputProducer` instances (`ps2-keyboard`, `ps2-mouse`), keyboard events + route to `keyboard_input`, mouse events to `mouse_input`, named-first with legacy fallback +- `usbhidd` migrated: one `InputProducer` per interface instance (`usb-{port}-if{n}`), named-first + with legacy fallback -**Remaining** (all require architectural changes to `inputd`, not USB-internal code): -- Migrate `usbhidd` toward named producers and per-device streams (requires inputd redesign) -- Expose hotplug add/remove behavior cleanly to downstream consumers (requires inputd redesign) -- Align USB HID with the `inputd` enhancement design already documented in-tree (cross-cutting) +**Remaining** (requires downstream consumer/driver migration, not inputd scheme changes): +- Migrate `i2c-hidd` to named producers (still uses legacy `ProducerHandle`) +- Expose hotplug add/remove behavior to downstream consumers via `evdevd` migration **Where**: `recipes/core/base/source/drivers/input/usbhidd/`, `inputd/`, `local/docs/INPUT-SCHEME-ENHANCEMENT.md` @@ -685,7 +707,7 @@ The remaining gaps now fall into two categories: **Broader architectural work (cross-cutting, not a small bounded USB-only fix):** - Any remaining USB composite/device-model issues now belong to wider device-model/design cleanup rather than one more isolated helper patch. -- HID producer modernization: per-device streams, hotplug add/remove (requires inputd redesign) +- HID producer modernization: per-device streams via named producers, hotplug add/remove (inputd redesign complete, ps2d and usbhidd migrated) - Userspace USB API: `libusb` WIP, no coherent native story **Hardware-dependent or design decisions:** diff --git a/local/patches/base/redox.patch b/local/patches/base/redox.patch index f02a7125..eb5a46ab 100644 --- a/local/patches/base/redox.patch +++ b/local/patches/base/redox.patch @@ -8517,6 +8517,40 @@ index d7af4cba..638b7cc1 100644 } } +diff --git a/drivers/input/ps2d/src/main.rs b/drivers/input/ps2d/src/main.rs +index db17de2a..1ae055e4 100644 +--- a/drivers/input/ps2d/src/main.rs ++++ b/drivers/input/ps2d/src/main.rs +@@ -11,7 +11,7 @@ use std::process; + + use common::acquire_port_io_rights; + use event::{user_data, EventQueue}; +-use inputd::ProducerHandle; ++use inputd::InputProducer; + + use crate::state::Ps2d; + +@@ -31,7 +31,10 @@ fn daemon(daemon: daemon::Daemon) -> ! { + + acquire_port_io_rights().expect("ps2d: failed to get I/O permission"); + +- let input = ProducerHandle::new().expect("ps2d: failed to open input producer"); ++ let keyboard_input = InputProducer::new_named_or_fallback("ps2-keyboard") ++ .expect("ps2d: failed to open input producer"); ++ let mouse_input = InputProducer::new_named_or_fallback("ps2-mouse") ++ .expect("ps2d: failed to open input producer"); + + user_data! { + enum Source { +@@ -93,7 +96,7 @@ fn daemon(daemon: daemon::Daemon) -> ! { + + daemon.ready(); + +- let mut ps2d = Ps2d::new(input, time_file); ++ let mut ps2d = Ps2d::new(keyboard_input, mouse_input, time_file); + + let mut data = [0; 256]; + for event in event_queue.map(|e| e.expect("ps2d: failed to get next event").user_data) { diff --git a/drivers/input/ps2d/src/mouse.rs b/drivers/input/ps2d/src/mouse.rs index 9e95ab88..8087c8c4 100644 --- a/drivers/input/ps2d/src/mouse.rs @@ -8651,12 +8685,31 @@ index 9e95ab88..8087c8c4 100644 MouseState::Streaming { id } => { MouseResult::Packet(data, id == MouseId::Intellimouse1 as u8) diff --git a/drivers/input/ps2d/src/state.rs b/drivers/input/ps2d/src/state.rs -index 9018dc6b..9e37b937 100644 +index 9018dc6b..da304e05 100644 --- a/drivers/input/ps2d/src/state.rs +++ b/drivers/input/ps2d/src/state.rs -@@ -61,7 +61,9 @@ pub struct Ps2d { +@@ -1,4 +1,4 @@ +-use inputd::ProducerHandle; ++use inputd::InputProducer; + use log::{error, warn}; + use orbclient::{ButtonEvent, KeyEvent, MouseEvent, MouseRelativeEvent, ScrollEvent}; + use std::{ +@@ -44,7 +44,8 @@ pub struct Ps2d { + ps2: Ps2, + vmmouse: bool, + vmmouse_relative: bool, +- input: ProducerHandle, ++ keyboard_input: InputProducer, ++ mouse_input: InputProducer, + time_file: File, + extended: bool, + mouse_x: i32, +@@ -59,9 +60,11 @@ pub struct Ps2d { + } + impl Ps2d { - pub fn new(input: ProducerHandle, time_file: File) -> Self { +- pub fn new(input: ProducerHandle, time_file: File) -> Self { ++ pub fn new(keyboard_input: InputProducer, mouse_input: InputProducer, time_file: File) -> Self { let mut ps2 = Ps2::new(); - ps2.init().expect("failed to initialize"); + if let Err(err) = ps2.init() { @@ -8665,8 +8718,120 @@ index 9018dc6b..9e37b937 100644 // FIXME add an option for orbital to disable this when an app captures the mouse. let vmmouse_relative = false; +@@ -77,7 +80,8 @@ impl Ps2d { + ps2, + vmmouse, + vmmouse_relative, +- input, ++ keyboard_input, ++ mouse_input, + time_file, + extended: false, + mouse_x: 0, +@@ -273,7 +277,7 @@ impl Ps2d { + }; + + if scancode != 0 { +- self.input ++ self.keyboard_input + .write_event( + KeyEvent { + character: '\0', +@@ -304,7 +308,7 @@ impl Ps2d { + + if self.vmmouse_relative { + if dx != 0 || dy != 0 { +- self.input ++ self.mouse_input + .write_event( + MouseRelativeEvent { + dx: dx as i32, +@@ -320,14 +324,14 @@ impl Ps2d { + if x != self.mouse_x || y != self.mouse_y { + self.mouse_x = x; + self.mouse_y = y; +- self.input ++ self.mouse_input + .write_event(MouseEvent { x, y }.to_event()) + .expect("ps2d: failed to write mouse event"); + } + }; + + if dz != 0 { +- self.input ++ self.mouse_input + .write_event( + ScrollEvent { + x: 0, +@@ -348,7 +352,7 @@ impl Ps2d { + self.mouse_left = left; + self.mouse_middle = middle; + self.mouse_right = right; +- self.input ++ self.mouse_input + .write_event( + ButtonEvent { + left, +@@ -441,13 +445,13 @@ impl Ps2d { + } + + if dx != 0 || dy != 0 { +- self.input ++ self.mouse_input + .write_event(MouseRelativeEvent { dx, dy }.to_event()) + .expect("ps2d: failed to write mouse event"); + } + + if dz != 0 { +- self.input ++ self.mouse_input + .write_event(ScrollEvent { x: 0, y: dz }.to_event()) + .expect("ps2d: failed to write scroll event"); + } +@@ -462,7 +466,7 @@ impl Ps2d { + self.mouse_left = left; + self.mouse_middle = middle; + self.mouse_right = right; +- self.input ++ self.mouse_input + .write_event( + ButtonEvent { + left, +diff --git a/drivers/input/usbhidd/src/main.rs b/drivers/input/usbhidd/src/main.rs +index 15c5b778..706c4008 100644 +--- a/drivers/input/usbhidd/src/main.rs ++++ b/drivers/input/usbhidd/src/main.rs +@@ -1,7 +1,7 @@ + use anyhow::{Context, Result}; + use std::{env, thread, time}; + +-use inputd::ProducerHandle; ++use inputd::InputProducer; + use orbclient::KeyEvent as OrbKeyEvent; + use rehid::{ + report_desc::{ReportTy, REPORT_DESC_TY}, +@@ -15,7 +15,7 @@ use xhcid_interface::{ + + mod reqs; + +-fn send_key_event(display: &mut ProducerHandle, usage_page: u16, usage: u16, pressed: bool) { ++fn send_key_event(display: &mut InputProducer, usage_page: u16, usage: u16, pressed: bool) { + let scancode = match usage_page { + 0x07 => match usage { + 0x04 => orbclient::K_A, +@@ -272,7 +272,9 @@ fn main() -> Result<()> { + let report_ty = ReportTy::Input; + let report_id = 0; + +- let mut display = ProducerHandle::new().context("Failed to open input socket")?; ++ let producer_name = format!("usb-{}-if{}", port, interface_num); ++ let mut display = InputProducer::new_named_or_fallback(&producer_name) ++ .context("Failed to open input socket")?; + let mut endpoint_opt = match endp_desc_opt { + Some((endp_num, _endp_desc)) => match handle.open_endpoint(endp_num as u8) { + Ok(ok) => Some(ok), diff --git a/drivers/inputd/src/lib.rs b/drivers/inputd/src/lib.rs -index b68e8211..0627f301 100644 +index b68e8211..f07a411d 100644 --- a/drivers/inputd/src/lib.rs +++ b/drivers/inputd/src/lib.rs @@ -64,25 +64,53 @@ impl ConsumerHandle { @@ -8753,11 +8918,452 @@ index b68e8211..0627f301 100644 Ok(Some(event)) } } +@@ -171,13 +203,11 @@ impl ControlHandle { + Ok(Self(File::open(path)?)) + } + +- /// Sent to Handle::Display + pub fn activate_vt(&mut self, vt: usize) -> io::Result { + let cmd = ControlEvent::from(VtActivate { vt }); + self.0.write(unsafe { any_as_u8_slice(&cmd) }) + } + +- /// Sent to Handle::Producer + pub fn activate_keymap(&mut self, keymap: usize) -> io::Result { + let cmd = ControlEvent::from(KeymapActivate { keymap }); + self.0.write(unsafe { any_as_u8_slice(&cmd) }) +@@ -209,3 +239,195 @@ impl ProducerHandle { + Ok(()) + } + } ++ ++pub struct NamedProducerHandle(File); ++ ++impl NamedProducerHandle { ++ pub fn new(name: &str) -> io::Result { ++ let path = format!("/scheme/input/producer/{name}"); ++ Ok(Self(File::open(path)?)) ++ } ++ ++ pub fn write_event(&mut self, event: orbclient::Event) -> io::Result<()> { ++ self.0.write(&event)?; ++ Ok(()) ++ } ++} ++ ++/// Convenience wrapper that tries a named producer first, ++/// falling back to the legacy anonymous producer on failure. ++pub enum InputProducer { ++ Named(NamedProducerHandle), ++ Legacy(ProducerHandle), ++} ++ ++impl InputProducer { ++ /// Open a named producer (`/scheme/input/producer/{name}`). ++ /// If the named path is unavailable, fall back to the legacy ++ /// `/scheme/input/producer` path so the driver keeps working on ++ /// older inputd builds or degraded schemes. ++ pub fn new_named_or_fallback(name: &str) -> io::Result { ++ match NamedProducerHandle::new(name) { ++ Ok(named) => Ok(InputProducer::Named(named)), ++ Err(named_err) => { ++ log::debug!( ++ "inputd: named producer '{}' unavailable ({}), falling back to legacy", ++ name, ++ named_err ++ ); ++ ProducerHandle::new().map(InputProducer::Legacy) ++ } ++ } ++ } ++ ++ /// Open the legacy anonymous producer directly. ++ pub fn new_legacy() -> io::Result { ++ ProducerHandle::new().map(InputProducer::Legacy) ++ } ++ ++ pub fn write_event(&mut self, event: orbclient::Event) -> io::Result<()> { ++ match self { ++ InputProducer::Named(h) => h.write_event(event), ++ InputProducer::Legacy(h) => h.write_event(event), ++ } ++ } ++} ++ ++pub struct DeviceConsumerHandle(File); ++ ++pub enum DeviceConsumerHandleEvent<'a> { ++ Events(&'a [Event]), ++} ++ ++impl DeviceConsumerHandle { ++ pub fn new(device_name: &str) -> io::Result { ++ let path = format!("/scheme/input/{device_name}"); ++ Ok(Self(File::open(path)?)) ++ } ++ ++ pub fn event_handle(&self) -> BorrowedFd<'_> { ++ self.0.as_fd() ++ } ++ ++ pub fn read_events<'a>( ++ &self, ++ events: &'a mut [Event], ++ ) -> io::Result> { ++ match read_to_slice(self.0.as_fd(), events) { ++ Ok(count) => Ok(DeviceConsumerHandleEvent::Events(&events[..count])), ++ Err(err) => Err(err.into()), ++ } ++ } ++} ++ ++#[derive(Debug, Clone)] ++#[repr(C)] ++pub struct HotplugEventHeader { ++ pub kind: u32, ++ pub device_id: u32, ++ pub name_len: u32, ++ pub reserved: u32, ++} ++ ++#[derive(Debug, Clone)] ++pub struct HotplugEvent { ++ pub kind: u32, ++ pub device_id: u32, ++ pub name: String, ++} ++ ++pub struct HotplugHandle { ++ file: File, ++ partial: Vec, ++} ++ ++impl HotplugHandle { ++ pub fn new() -> io::Result { ++ let file = File::open("/scheme/input/events")?; ++ Ok(Self { ++ file, ++ partial: Vec::new(), ++ }) ++ } ++ ++ pub fn event_handle(&self) -> BorrowedFd<'_> { ++ self.file.as_fd() ++ } ++ ++ pub fn read_event(&mut self) -> io::Result> { ++ let mut tmp = [0u8; 256]; ++ match self.file.read(&mut tmp) { ++ Ok(0) => {} ++ Ok(n) => self.partial.extend_from_slice(&tmp[..n]), ++ Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {} ++ Err(e) => return Err(e), ++ } ++ ++ if self.partial.len() < 16 { ++ return Ok(None); ++ } ++ ++ let header = HotplugEventHeader { ++ kind: u32::from_ne_bytes(self.partial[0..4].try_into().map_err(|_| { ++ io::Error::new(io::ErrorKind::InvalidData, "header parse failed") ++ })?), ++ device_id: u32::from_ne_bytes(self.partial[4..8].try_into().map_err(|_| { ++ io::Error::new(io::ErrorKind::InvalidData, "header parse failed") ++ })?), ++ name_len: u32::from_ne_bytes(self.partial[8..12].try_into().map_err(|_| { ++ io::Error::new(io::ErrorKind::InvalidData, "header parse failed") ++ })?), ++ reserved: 0, ++ }; ++ ++ let total_len = 16 + header.name_len as usize; ++ if self.partial.len() < total_len { ++ return Ok(None); ++ } ++ ++ let name = String::from_utf8(self.partial[16..total_len].to_vec()).map_err(|e| { ++ io::Error::new(io::ErrorKind::InvalidData, format!("invalid UTF-8: {e}")) ++ })?; ++ ++ self.partial.drain(..total_len); ++ ++ Ok(Some(HotplugEvent { ++ kind: header.kind, ++ device_id: header.device_id, ++ name, ++ })) ++ } ++} ++ ++pub const RESERVED_DEVICE_NAMES: &[&str] = &[ ++ "producer", ++ "consumer", ++ "consumer_bootlog", ++ "events", ++ "handle", ++ "handle_early", ++ "control", ++]; ++ ++pub struct InputDeviceLister; ++ ++impl InputDeviceLister { ++ pub fn list() -> io::Result> { ++ let mut dir = std::fs::read_dir("/scheme/input/")?; ++ let mut devices = Vec::new(); ++ loop { ++ match dir.next() { ++ Some(Ok(entry)) => { ++ if let Some(name) = entry.file_name().to_str() { ++ if !RESERVED_DEVICE_NAMES.contains(&name) { ++ devices.push(name.to_owned()); ++ } ++ } ++ } ++ Some(Err(e)) => return Err(e), ++ None => break, ++ } ++ } ++ Ok(devices) ++ } ++} diff --git a/drivers/inputd/src/main.rs b/drivers/inputd/src/main.rs -index 07aa943e..61641b9f 100644 +index 07aa943e..89018568 100644 --- a/drivers/inputd/src/main.rs +++ b/drivers/inputd/src/main.rs -@@ -274,7 +274,7 @@ impl SchemeSync for InputScheme { +@@ -13,7 +13,7 @@ + + use core::mem::size_of; + use std::borrow::Cow; +-use std::collections::BTreeSet; ++use std::collections::{BTreeMap, BTreeSet}; + use std::mem::transmute; + use std::ops::ControlFlow; + use std::sync::atomic::{AtomicUsize, Ordering}; +@@ -26,8 +26,9 @@ use redox_scheme::{CallerCtx, OpenResult, Response, SignalBehavior, Socket}; + + use orbclient::{Event, EventOption}; + use scheme_utils::{Blocking, FpathWriter, HandleMap}; ++use syscall::dirent::{DirEntry, DirentBuf, DirentKind}; + use syscall::schemev2::NewFdFlags; +-use syscall::{Error as SysError, EventFlags, EACCES, EBADF, EEXIST, EINVAL}; ++use syscall::{Error as SysError, EventFlags, EACCES, EBADF, EEXIST, EINVAL, ENOENT, ENOTDIR}; + + pub mod keymap; + +@@ -35,8 +36,57 @@ use keymap::KeymapKind; + + use crate::keymap::KeymapData; + ++const DEVICE_ADD: u32 = 1; ++const DEVICE_REMOVE: u32 = 2; ++ ++fn validate_producer_name(name: &str) -> Result<(), SysError> { ++ if name.is_empty() || name.contains('/') { ++ return Err(SysError::new(EINVAL)); ++ } ++ if inputd::RESERVED_DEVICE_NAMES.contains(&name) { ++ return Err(SysError::new(EINVAL)); ++ } ++ Ok(()) ++} ++ ++fn serialize_hotplug(kind: u32, device_id: u32, name: &str) -> Vec { ++ let name_bytes = name.as_bytes(); ++ let header = HotplugHeader { ++ kind, ++ device_id, ++ name_len: name_bytes.len() as u32, ++ _reserved: 0, ++ }; ++ let mut out = Vec::with_capacity(16 + name_bytes.len()); ++ out.extend_from_slice(&header.to_bytes()); ++ out.extend_from_slice(name_bytes); ++ out ++} ++ ++#[repr(C)] ++struct HotplugHeader { ++ kind: u32, ++ device_id: u32, ++ name_len: u32, ++ _reserved: u32, ++} ++ ++impl HotplugHeader { ++ fn to_bytes(&self) -> [u8; 16] { ++ let mut buf = [0u8; 16]; ++ buf[0..4].copy_from_slice(&self.kind.to_ne_bytes()); ++ buf[4..8].copy_from_slice(&self.device_id.to_ne_bytes()); ++ buf[8..12].copy_from_slice(&self.name_len.to_ne_bytes()); ++ buf[12..16].copy_from_slice(&self._reserved.to_ne_bytes()); ++ buf ++ } ++} ++ + enum Handle { + Producer, ++ NamedProducer { ++ name: String, ++ }, + Consumer { + events: EventFlags, + pending: Vec, +@@ -46,6 +96,17 @@ enum Handle { + notified: bool, + vt: usize, + }, ++ DeviceConsumer { ++ device_name: String, ++ events: EventFlags, ++ pending: Vec, ++ notified: bool, ++ }, ++ HotplugEvents { ++ events: EventFlags, ++ pending: Vec, ++ notified: bool, ++ }, + Display { + events: EventFlags, + pending: Vec, +@@ -72,6 +133,9 @@ struct InputScheme { + rshift: bool, + + has_new_events: bool, ++ ++ devices: BTreeMap, ++ next_device_id: AtomicUsize, + } + + impl InputScheme { +@@ -90,9 +154,28 @@ impl InputScheme { + lshift: false, + rshift: false, + has_new_events: false, ++ ++ devices: BTreeMap::new(), ++ next_device_id: AtomicUsize::new(1), + } + } + ++ fn emit_hotplug(&mut self, kind: u32, device_id: u32, name: &str) { ++ let record = serialize_hotplug(kind, device_id, name); ++ for handle in self.handles.values_mut() { ++ if let Handle::HotplugEvents { ++ pending, ++ notified, ++ .. ++ } = handle ++ { ++ pending.extend_from_slice(&record); ++ *notified = false; ++ } ++ } ++ self.has_new_events = true; ++ } ++ + fn switch_vt(&mut self, new_active: usize) { + if let Some(active_vt) = self.active_vt { + if new_active == active_vt { +@@ -146,6 +229,43 @@ impl InputScheme { + + self.active_keymap = KeymapData::new(new_active.into()); + } ++ ++ fn deliver_to_legacy_consumers(&mut self, buf: &[u8]) { ++ if let Some(active_vt) = self.active_vt { ++ for handle in self.handles.values_mut() { ++ if let Handle::Consumer { ++ pending, ++ notified, ++ vt, ++ .. ++ } = handle ++ { ++ if *vt != active_vt { ++ continue; ++ } ++ pending.extend_from_slice(buf); ++ *notified = false; ++ } ++ } ++ } ++ } ++ ++ fn deliver_to_device_consumers(&mut self, name: &str, buf: &[u8]) { ++ for handle in self.handles.values_mut() { ++ if let Handle::DeviceConsumer { ++ device_name, ++ pending, ++ notified, ++ .. ++ } = handle ++ { ++ if device_name == name { ++ pending.extend_from_slice(buf); ++ *notified = false; ++ } ++ } ++ } ++ } + } + + impl SchemeSync for InputScheme { +@@ -170,7 +290,23 @@ impl SchemeSync for InputScheme { + let command = path_parts.next().ok_or(SysError::new(EINVAL))?; + + let handle_ty = match command { +- "producer" => Handle::Producer, ++ "producer" => { ++ if let Some(name) = path_parts.next() { ++ validate_producer_name(name)?; ++ if self.devices.contains_key(name) { ++ return Err(SysError::new(EEXIST)); ++ } ++ let device_id = self.next_device_id.fetch_add(1, Ordering::SeqCst) as u32; ++ self.devices.insert(name.to_owned(), device_id); ++ let handle = Handle::NamedProducer { ++ name: name.to_owned(), ++ }; ++ self.emit_hotplug(DEVICE_ADD, device_id, name); ++ handle ++ } else { ++ Handle::Producer ++ } ++ } + "consumer" => { + let vt = self.next_vt_id.fetch_add(1, Ordering::Relaxed); + self.vts.insert(vt); +@@ -253,9 +389,23 @@ impl SchemeSync for InputScheme { + } + "control" => Handle::Control, + +- _ => { +- log::error!("invalid path '{path}'"); +- return Err(SysError::new(EINVAL)); ++ "events" => Handle::HotplugEvents { ++ events: EventFlags::empty(), ++ pending: Vec::new(), ++ notified: false, ++ }, ++ ++ // dynamic device consumer: must be a currently registered device ++ name => { ++ if !self.devices.contains_key(name) { ++ return Err(SysError::new(ENOENT)); ++ } ++ Handle::DeviceConsumer { ++ device_name: name.to_owned(), ++ events: EventFlags::empty(), ++ pending: Vec::new(), ++ notified: false, ++ } + } + }; + +@@ -274,7 +424,7 @@ impl SchemeSync for InputScheme { let handle = self.handles.get(id)?; if let Handle::Consumer { vt, .. } = handle { @@ -8766,18 +9372,205 @@ index 07aa943e..61641b9f 100644 Ok(()) } else { Err(SysError::new(EINVAL)) -@@ -438,7 +438,9 @@ impl SchemeSync for InputScheme { +@@ -282,6 +432,50 @@ impl SchemeSync for InputScheme { + }) + } + ++ fn getdents<'buf>( ++ &mut self, ++ id: usize, ++ mut buf: DirentBuf<&'buf mut [u8]>, ++ opaque_offset: u64, ++ ) -> syscall::Result> { ++ let handle = self.handles.get(id)?; ++ if !matches!(handle, Handle::SchemeRoot) { ++ return Err(SysError::new(ENOTDIR)); ++ } ++ ++ let static_entries: &[&str] = &[ ++ "producer", ++ "consumer", ++ "consumer_bootlog", ++ "events", ++ "handle", ++ "handle_early", ++ "control", ++ ]; ++ ++ let device_names: Vec<&str> = self.devices.keys().map(|s| s.as_str()).collect(); ++ ++ let all_entries: Vec<(&str, DirentKind)> = static_entries ++ .iter() ++ .map(|&name| (name, DirentKind::Directory)) ++ .chain(device_names.iter().map(|&name| (name, DirentKind::Unspecified))) ++ .collect(); ++ ++ for (idx, (name, kind)) in all_entries ++ .iter() ++ .enumerate() ++ .skip(opaque_offset as usize) ++ { ++ buf.entry(DirEntry { ++ inode: 0, ++ next_opaque_id: idx as u64 + 1, ++ name, ++ kind: *kind, ++ })?; ++ } ++ Ok(buf) ++ } ++ + fn read( + &mut self, + id: usize, +@@ -313,6 +507,22 @@ impl SchemeSync for InputScheme { + Ok(copy) + } + ++ Handle::DeviceConsumer { pending, .. } => { ++ let copy = core::cmp::min(pending.len(), buf.len()); ++ for (i, byte) in pending.drain(..copy).enumerate() { ++ buf[i] = byte; ++ } ++ Ok(copy) ++ } ++ ++ Handle::HotplugEvents { pending, .. } => { ++ let copy = core::cmp::min(pending.len(), buf.len()); ++ for (i, byte) in pending.drain(..copy).enumerate() { ++ buf[i] = byte; ++ } ++ Ok(copy) ++ } ++ + Handle::Display { pending, .. } => { + if buf.len() % size_of::() == 0 { + let copy = core::cmp::min(pending.len(), buf.len() / size_of::()); +@@ -334,6 +544,10 @@ impl SchemeSync for InputScheme { + log::error!("producer tried to read"); + return Err(SysError::new(EINVAL)); + } ++ Handle::NamedProducer { .. } => { ++ log::error!("named producer tried to read"); ++ return Err(SysError::new(EINVAL)); ++ } + Handle::Control => { + log::error!("control tried to read"); + return Err(SysError::new(EINVAL)); +@@ -379,11 +593,20 @@ impl SchemeSync for InputScheme { + log::error!("consumer tried to write"); + return Err(SysError::new(EINVAL)); + } ++ Handle::DeviceConsumer { .. } => { ++ log::error!("device consumer tried to write"); ++ return Err(SysError::new(EINVAL)); ++ } ++ Handle::HotplugEvents { .. } => { ++ log::error!("hotplug events tried to write"); ++ return Err(SysError::new(EINVAL)); ++ } + Handle::Display { .. } => { + log::error!("display tried to write"); + return Err(SysError::new(EINVAL)); + } + Handle::Producer => {} ++ Handle::NamedProducer { .. } => {} + Handle::SchemeRoot => return Err(SysError::new(EBADF)), } - let handle = self.handles.get_mut(id)?; -- assert!(matches!(handle, Handle::Producer)); -+ if !matches!(handle, Handle::Producer) { -+ return Err(SysError::new(EBADF)); -+ } +@@ -397,6 +620,11 @@ impl SchemeSync for InputScheme { + buf.len() / size_of::(), + ) + }); ++ let producer_name = match self.handles.get(id)? { ++ Handle::NamedProducer { ref name } => Some(name.clone()), ++ Handle::Producer => None, ++ _ => return Err(SysError::new(EBADF)), ++ }; - let buf = unsafe { + for i in 0..events.len() { + let mut new_active_opt = None; +@@ -437,38 +665,21 @@ impl SchemeSync for InputScheme { + } + } + +- let handle = self.handles.get_mut(id)?; +- assert!(matches!(handle, Handle::Producer)); +- +- let buf = unsafe { ++ let serialized = unsafe { core::slice::from_raw_parts( -@@ -505,8 +507,8 @@ impl SchemeSync for InputScheme { + (events.as_ptr()) as *const u8, + events.len() * size_of::(), + ) + }; + +- if let Some(active_vt) = self.active_vt { +- for handle in self.handles.values_mut() { +- match handle { +- Handle::Consumer { +- pending, +- notified, +- vt, +- .. +- } => { +- if *vt != active_vt { +- continue; +- } +- +- pending.extend_from_slice(buf); +- *notified = false; +- } +- _ => continue, +- } +- } ++ if let Some(ref name) = producer_name { ++ self.deliver_to_device_consumers(name, serialized); + } + +- Ok(buf.len()) ++ // named producers also feed the legacy path; legacy producers only feed legacy ++ self.deliver_to_legacy_consumers(serialized); ++ ++ Ok(serialized.len()) + } + + fn fevent( +@@ -487,6 +698,24 @@ impl SchemeSync for InputScheme { + *notified = false; + Ok(EventFlags::empty()) + } ++ Handle::DeviceConsumer { ++ ref mut events, ++ ref mut notified, ++ .. ++ } => { ++ *events = flags; ++ *notified = false; ++ Ok(EventFlags::empty()) ++ } ++ Handle::HotplugEvents { ++ ref mut events, ++ ref mut notified, ++ .. ++ } => { ++ *events = flags; ++ *notified = false; ++ Ok(EventFlags::empty()) ++ } + Handle::Display { + ref mut events, + ref mut notified, +@@ -496,7 +725,7 @@ impl SchemeSync for InputScheme { + *notified = false; + Ok(EventFlags::empty()) + } +- Handle::Producer | Handle::Control => { ++ Handle::Producer | Handle::NamedProducer { .. } | Handle::Control => { + log::error!("producer or control tried to use an event queue"); + Err(SysError::new(EINVAL)) + } +@@ -505,8 +734,8 @@ impl SchemeSync for InputScheme { } fn on_close(&mut self, id: usize) { @@ -8788,11 +9581,16 @@ index 07aa943e..61641b9f 100644 self.vts.remove(&vt); if self.active_vt == Some(vt) { if let Some(&new_vt) = self.vts.last() { -@@ -516,7 +518,10 @@ impl SchemeSync for InputScheme { +@@ -516,7 +745,15 @@ impl SchemeSync for InputScheme { } } } - _ => {} ++ Some(Handle::NamedProducer { name, .. }) => { ++ if let Some(device_id) = self.devices.remove(&name) { ++ self.emit_hotplug(DEVICE_REMOVE, device_id, &name); ++ } ++ } + Some(_) => {} + None => { + log::warn!("inputd: on_close called with unknown handle id {id}"); @@ -8800,7 +9598,47 @@ index 07aa943e..61641b9f 100644 } } } -@@ -589,8 +594,11 @@ fn deamon(daemon: daemon::SchemeDaemon) -> anyhow::Result<()> { +@@ -564,6 +801,39 @@ fn deamon(daemon: daemon::SchemeDaemon) -> anyhow::Result<()> { + + *notified = true; + } ++ Handle::DeviceConsumer { ++ events, ++ pending, ++ ref mut notified, ++ .. ++ } => { ++ if pending.is_empty() || *notified || !events.contains(EventFlags::EVENT_READ) { ++ continue; ++ } ++ ++ socket_file.write_response( ++ Response::post_fevent(*id, EventFlags::EVENT_READ.bits()), ++ SignalBehavior::Restart, ++ )?; ++ ++ *notified = true; ++ } ++ Handle::HotplugEvents { ++ events, ++ pending, ++ ref mut notified, ++ } => { ++ if pending.is_empty() || *notified || !events.contains(EventFlags::EVENT_READ) { ++ continue; ++ } ++ ++ socket_file.write_response( ++ Response::post_fevent(*id, EventFlags::EVENT_READ.bits()), ++ SignalBehavior::Restart, ++ )?; ++ ++ *notified = true; ++ } + Handle::Display { + events, + pending, +@@ -589,8 +859,11 @@ fn deamon(daemon: daemon::SchemeDaemon) -> anyhow::Result<()> { } fn daemon_runner(daemon: daemon::SchemeDaemon) -> ! { @@ -8814,7 +9652,7 @@ index 07aa943e..61641b9f 100644 } const HELP: &str = r#" -@@ -608,13 +616,26 @@ fn main() { +@@ -608,13 +881,26 @@ fn main() { match val.as_ref() { // Activates a VT. "-A" => { @@ -8847,7 +9685,7 @@ index 07aa943e..61641b9f 100644 } // Activates a keymap. "-K" => { -@@ -630,11 +651,17 @@ fn main() { +@@ -630,11 +916,17 @@ fn main() { std::process::exit(1); }); @@ -8870,7 +9708,7 @@ index 07aa943e..61641b9f 100644 } // List available keymaps "--keymaps" => { -@@ -647,7 +674,10 @@ fn main() { +@@ -647,7 +939,10 @@ fn main() { println!("{}", HELP); } @@ -13384,7 +14222,7 @@ index ac492d5b..310fe51f 100644 let ring_ref = match endpoint_state.transfer { diff --git a/drivers/usb/xhcid/src/xhci/mod.rs b/drivers/usb/xhcid/src/xhci/mod.rs -index f2143676..9ce15161 100644 +index f2143676..0d2ec432 100644 --- a/drivers/usb/xhcid/src/xhci/mod.rs +++ b/drivers/usb/xhcid/src/xhci/mod.rs @@ -11,12 +11,13 @@ @@ -13484,7 +14322,11 @@ index f2143676..9ce15161 100644 drivers: CHashMap>, scheme_name: String, -@@ -311,6 +368,93 @@ struct PortState { +@@ -308,9 +365,97 @@ struct PortState { + slot: u8, + protocol_speed: &'static ProtocolSpeed, + cfg_idx: Option, ++ active_ifaces: BTreeMap, // iface number → active alternate setting input_context: Mutex>>, dev_desc: Option, endpoint_states: BTreeMap, @@ -13578,7 +14420,7 @@ index f2143676..9ce15161 100644 } impl PortState { -@@ -463,6 +607,7 @@ impl Xhci { +@@ -463,6 +608,7 @@ impl Xhci { handles: CHashMap::new(), next_handle: AtomicUsize::new(0), port_states: CHashMap::new(), @@ -13586,7 +14428,7 @@ index f2143676..9ce15161 100644 drivers: CHashMap::new(), scheme_name, -@@ -793,11 +938,14 @@ impl Xhci { +@@ -793,11 +939,14 @@ impl Xhci { } pub async fn attach_device(&self, port_id: PortId) -> syscall::Result<()> { @@ -13602,7 +14444,7 @@ index f2143676..9ce15161 100644 let (data, state, speed, flags) = { let port = &self.ports.lock().unwrap()[port_id.root_hub_port_index()]; (port.read(), port.state(), port.speed(), port.flags()) -@@ -808,74 +956,101 @@ impl Xhci { +@@ -808,74 +957,102 @@ impl Xhci { port_id, data, state, speed, flags ); @@ -13729,6 +14571,7 @@ index f2143676..9ce15161 100644 + input_context: Mutex::new(input), + dev_desc: None, + cfg_idx: None, ++ active_ifaces: BTreeMap::new(), + endpoint_states: std::iter::once(( + 0, + EndpointState { @@ -13760,7 +14603,7 @@ index f2143676..9ce15161 100644 self.update_max_packet_size(&mut *input, slot, dev_desc_8_byte) .await?; -@@ -885,97 +1060,175 @@ impl Xhci { +@@ -885,97 +1062,175 @@ impl Xhci { let dev_desc = self.get_desc(port_id, slot).await?; debug!("Got the full device descriptor!"); @@ -13796,7 +14639,11 @@ index f2143676..9ce15161 100644 + Ok(()) + } + .await; -+ + +- match self.spawn_drivers(port_id) { +- Ok(()) => (), +- Err(err) => { +- error!("Failed to spawn driver for port {}: `{}`", port_id, err) + match attach_result { + Ok(()) => { + if let Some(delay_ms) = @@ -13807,19 +14654,15 @@ index f2143676..9ce15161 100644 + port_id, delay_ms + ); + thread::sleep(Duration::from_millis(delay_ms)); -+ } - -- match self.spawn_drivers(port_id) { -- Ok(()) => (), -- Err(err) => { -- error!("Failed to spawn driver for port {}: `{}`", port_id, err) + } ++ + if lifecycle.finish_attach_success() != PortLifecycleState::Attached { + warn!( + "attach for port {} completed after detach already started; skipping publication", + port_id + ); + return Err(Error::new(EBUSY)); - } ++ } + + let staged_port_state = self + .staged_port_states @@ -13999,6 +14842,26 @@ index f2143676..9ce15161 100644 } } +@@ -1246,14 +1501,12 @@ impl Xhci { + let drivers_usercfg: &DriversConfig = &DRIVERS_CONFIG; + + for ifdesc in config_desc.interface_descs.iter() { +- //TODO: support alternate settings +- // This is difficult because the device driver must know which alternate +- // to use, but if alternates can have different classes, then a different +- // device driver may be required for each alternate. For now, we will use +- // only the default alternate setting (0) ++ // Only auto-spawn drivers for the default alternate setting (0). ++ // Non-default alternates are selected later by the device driver ++ // via SET_INTERFACE + configure_endpoints with specific alternate_setting. + if ifdesc.alternate_setting != 0 { +- warn!( +- "ignoring port {} iface {} alternate {} class {}.{} proto {}", ++ debug!( ++ "skipping port {} iface {} alternate {} class {}.{} proto {} (non-default alternate)", + port, + ifdesc.number, + ifdesc.alternate_setting, @@ -1458,6 +1711,53 @@ pub fn start_device_enumerator(hci: &Arc>) { })); } @@ -14054,7 +14917,7 @@ index f2143676..9ce15161 100644 struct DriverConfig { name: String, diff --git a/drivers/usb/xhcid/src/xhci/scheme.rs b/drivers/usb/xhcid/src/xhci/scheme.rs -index ca27b3fe..468a98ae 100644 +index ca27b3fe..29437294 100644 --- a/drivers/usb/xhcid/src/xhci/scheme.rs +++ b/drivers/usb/xhcid/src/xhci/scheme.rs @@ -20,6 +20,7 @@ use std::convert::TryFrom; @@ -14266,7 +15129,19 @@ index ca27b3fe..468a98ae 100644 let endp_idx = endp_num.checked_sub(1).ok_or(Error::new(EIO))?; let mut port_state = self.port_state_mut(port_num)?; -@@ -950,35 +1030,102 @@ impl Xhci { +@@ -835,7 +915,10 @@ impl Xhci { + port, + usb::Setup::set_interface(interface_num, alternate_setting), + ) +- .await ++ .await?; ++ let mut port_state = self.port_states.get_mut(&port).ok_or(Error::new(EBADFD))?; ++ port_state.active_ifaces.insert(interface_num, alternate_setting); ++ Ok(()) + } + + async fn reset_endpoint(&self, port_num: PortId, endp_num: u8, tsp: bool) -> Result<()> { +@@ -950,35 +1033,114 @@ impl Xhci { self.port_states.get_mut(&port).ok_or(Error::new(EBADF)) } @@ -14372,11 +15247,23 @@ index ca27b3fe..468a98ae 100644 - new_context_entries = entry; - } + let configuration_value = config_desc.configuration_value; -+ let endpoint_descs = config_desc -+ .interface_descs -+ .iter() -+ .flat_map(|if_desc| if_desc.endpoints.iter().copied()) -+ .collect::>(); ++ ++ let endpoint_descs = if let Some(iface_num) = req.interface_desc { ++ let alt = req.alternate_setting.unwrap_or(0); ++ config_desc ++ .interface_descs ++ .iter() ++ .filter(|if_desc| if_desc.number == iface_num && if_desc.alternate_setting == alt) ++ .flat_map(|if_desc| if_desc.endpoints.iter().copied()) ++ .collect::>() ++ } else { ++ config_desc ++ .interface_descs ++ .iter() ++ .filter(|if_desc| if_desc.alternate_setting == 0) ++ .flat_map(|if_desc| if_desc.endpoints.iter().copied()) ++ .collect::>() ++ }; + + let endp_desc_count = endpoint_descs.len(); + let mut new_context_entries = 1u8; @@ -14387,7 +15274,7 @@ index ca27b3fe..468a98ae 100644 } } new_context_entries += 1; -@@ -989,74 +1136,22 @@ impl Xhci { +@@ -989,74 +1151,22 @@ impl Xhci { } ( @@ -14451,7 +15338,9 @@ index ca27b3fe..468a98ae 100644 - }; - input_context.control.write(control); - } -- ++ let mut staged_endpoint_states = BTreeMap::new(); ++ let mut endpoint_programs = Vec::new(); + - for endp_idx in 0..endp_desc_count as u8 { - let endp_num = endp_idx + 1; - @@ -14461,9 +15350,7 @@ index ca27b3fe..468a98ae 100644 - warn!("failed to find endpoint {}", endp_idx); - Error::new(EIO) - })?; -+ let mut staged_endpoint_states = BTreeMap::new(); -+ let mut endpoint_programs = Vec::new(); - +- - let endp_num_xhc = Self::endp_num_to_dci(endp_num, endp_desc); + for (endp_idx, endp_desc) in endpoint_descs.iter().copied().enumerate() { + let endp_num = endp_idx as u8 + 1; @@ -14471,7 +15358,7 @@ index ca27b3fe..468a98ae 100644 let usb_log_max_streams = endp_desc.log_max_streams(); -@@ -1078,20 +1173,20 @@ impl Xhci { +@@ -1078,20 +1188,20 @@ impl Xhci { let mult = endp_desc.isoch_mult(lec); @@ -14497,7 +15384,7 @@ index ca27b3fe..468a98ae 100644 let max_error_count = 3; let ep_ty = endp_desc.xhci_ep_type()?; -@@ -1114,7 +1209,7 @@ impl Xhci { +@@ -1114,7 +1224,7 @@ impl Xhci { assert_eq!(max_error_count & 0x3, max_error_count); assert_ne!(ep_ty, 0); // 0 means invalid. @@ -14506,7 +15393,7 @@ index ca27b3fe..468a98ae 100644 let mut array = StreamContextArray::new::(self.cap.ac64(), 1 << (primary_streams + 1))?; -@@ -1127,15 +1222,13 @@ impl Xhci { +@@ -1127,15 +1237,13 @@ impl Xhci { array_ptr, "stream ctx ptr not aligned to 16 bytes" ); @@ -14525,7 +15412,7 @@ index ca27b3fe..468a98ae 100644 } else { let ring = Ring::new::(self.cap.ac64(), 16, true)?; let ring_ptr = ring.register(); -@@ -1145,68 +1238,185 @@ impl Xhci { +@@ -1145,68 +1253,205 @@ impl Xhci { ring_ptr, "ring pointer not aligned to 16 bytes" ); @@ -14621,13 +15508,12 @@ index ca27b3fe..468a98ae 100644 }) - .await; + .collect::>(); - -- //self.event_handler_finished(); ++ + // Configure the slot context as well, which holds the last index of the endp descs. + input_context.add_context.write(1); + input_context.drop_context.write(0); -- handle_event_trb("CONFIGURE_ENDPOINT", &event_trb, &command_trb)?; +- //self.event_handler_finished(); + const CONTEXT_ENTRIES_MASK: u32 = 0xF800_0000; + const CONTEXT_ENTRIES_SHIFT: u8 = 27; + @@ -14689,10 +15575,8 @@ index ca27b3fe..468a98ae 100644 + ) + .await; + return Err(err); - } - -- // Tell the device about this configuration. -- self.set_configuration(port, configuration_value).await?; ++ } ++ + if self.consume_test_hook("fail_after_configure_endpoint") { + info!( + "xhcid: test hook injecting failure after CONFIGURE_ENDPOINT for port {}", @@ -14720,7 +15604,8 @@ index ca27b3fe..468a98ae 100644 + .await; + return Err(err); + } -+ + +- handle_event_trb("CONFIGURE_ENDPOINT", &event_trb, &command_trb)?; + if self.consume_test_hook("fail_after_set_configuration") { + info!( + "xhcid: test hook injecting failure after SET_CONFIGURATION for port {}", @@ -14735,8 +15620,10 @@ index ca27b3fe..468a98ae 100644 + ) + .await; + return Err(Error::new(EIO)); -+ } -+ + } + +- // Tell the device about this configuration. +- self.set_configuration(port, configuration_value).await?; + { + let mut port_state = self.port_states.get_mut(&port).ok_or(Error::new(EBADFD))?; + port_state.cfg_idx = Some(configuration_value); @@ -14744,11 +15631,31 @@ index ca27b3fe..468a98ae 100644 + for (endp_num, endpoint_state) in staged_endpoint_states { + port_state.endpoint_states.insert(endp_num, endpoint_state); + } ++ if let Some(iface_num) = req.interface_desc { ++ let alt = req.alternate_setting.unwrap_or(0); ++ port_state.active_ifaces.insert(iface_num, alt); ++ } else if port_state.active_ifaces.is_empty() { ++ let default_iface_entries: Vec<(u8, u8)> = port_state ++ .dev_desc ++ .as_ref() ++ .and_then(|dd| dd.config_descs.iter().find(|cd| cd.configuration_value == configuration_value)) ++ .map(|cd| { ++ cd.interface_descs ++ .iter() ++ .filter(|if_desc| if_desc.alternate_setting == 0) ++ .map(|if_desc| (if_desc.number, 0u8)) ++ .collect() ++ }) ++ .unwrap_or_default(); ++ for (iface_num, alt) in default_iface_entries { ++ port_state.active_ifaces.insert(iface_num, alt); ++ } ++ } + } Ok(()) } -@@ -1857,7 +2067,7 @@ impl Xhci { +@@ -1857,7 +2102,7 @@ impl Xhci { if (flags & O_DIRECTORY != 0) || (flags & O_STAT != 0) { let mut contents = Vec::new(); @@ -14757,7 +15664,7 @@ index ca27b3fe..468a98ae 100644 if self.slot_state( self.port_states -@@ -1894,6 +2104,14 @@ impl Xhci { +@@ -1894,6 +2139,14 @@ impl Xhci { Ok(Handle::PortState(port_num)) } @@ -14772,7 +15679,7 @@ index ca27b3fe..468a98ae 100644 /// implements open() for /port/endpoints /// /// # Arguments -@@ -2088,6 +2306,30 @@ impl Xhci { +@@ -2088,6 +2341,30 @@ impl Xhci { Ok(Handle::DetachDevice(port_num)) } @@ -14803,7 +15710,7 @@ index ca27b3fe..468a98ae 100644 /// implements open() for /port/request /// /// # Arguments -@@ -2156,6 +2398,9 @@ impl SchemeSync for &Xhci { +@@ -2156,6 +2433,9 @@ impl SchemeSync for &Xhci { SchemeParameters::PortState(port_number) => { self.open_handle_port_state(port_number, flags)? } @@ -14813,7 +15720,7 @@ index ca27b3fe..468a98ae 100644 SchemeParameters::PortReq(port_number) => { self.open_handle_port_request(port_number, flags)? } -@@ -2174,6 +2419,12 @@ impl SchemeSync for &Xhci { +@@ -2174,6 +2454,12 @@ impl SchemeSync for &Xhci { SchemeParameters::DetachDevice(port_number) => { self.open_handle_detach_device(port_number, flags)? } @@ -14826,7 +15733,7 @@ index ca27b3fe..468a98ae 100644 }; let fd = self.next_handle.fetch_add(1, atomic::Ordering::Relaxed); -@@ -2204,7 +2455,11 @@ impl SchemeSync for &Xhci { +@@ -2204,7 +2490,11 @@ impl SchemeSync for &Xhci { //If we have a handle to the configure scheme, we need to mark it as write only. match &*guard { @@ -14839,7 +15746,7 @@ index ca27b3fe..468a98ae 100644 stat.st_mode = stat.st_mode | 0o200; } _ => {} -@@ -2254,6 +2509,8 @@ impl SchemeSync for &Xhci { +@@ -2254,6 +2544,8 @@ impl SchemeSync for &Xhci { Handle::ConfigureEndpoints(_) => Err(Error::new(EBADF)), Handle::AttachDevice(_) => Err(Error::new(EBADF)), Handle::DetachDevice(_) => Err(Error::new(EBADF)), @@ -14848,7 +15755,7 @@ index ca27b3fe..468a98ae 100644 Handle::SchemeRoot => Err(Error::new(EBADF)), &mut Handle::Endpoint(port_num, endp_num, ref mut st) => match st { -@@ -2285,6 +2542,10 @@ impl SchemeSync for &Xhci { +@@ -2285,6 +2577,10 @@ impl SchemeSync for &Xhci { Ok(Xhci::::write_dyn_string(string, buf, offset)) } @@ -14859,7 +15766,7 @@ index ca27b3fe..468a98ae 100644 &mut Handle::PortReq(port_num, ref mut st) => { let state = std::mem::replace(st, PortReqState::Tmp); drop(guard); // release the lock -@@ -2324,6 +2585,14 @@ impl SchemeSync for &Xhci { +@@ -2324,6 +2620,14 @@ impl SchemeSync for &Xhci { block_on(self.detach_device(port_num))?; Ok(buf.len()) } @@ -14874,7 +15781,7 @@ index ca27b3fe..468a98ae 100644 &mut Handle::Endpoint(port_num, endp_num, ref ep_file_ty) => match ep_file_ty { EndpointHandleTy::Ctl => block_on(self.on_write_endp_ctl(port_num, endp_num, buf)), EndpointHandleTy::Data => { -@@ -2348,6 +2617,54 @@ impl SchemeSync for &Xhci { +@@ -2348,6 +2652,54 @@ impl SchemeSync for &Xhci { } impl Xhci { @@ -14929,7 +15836,7 @@ index ca27b3fe..468a98ae 100644 pub fn get_endp_status(&self, port_num: PortId, endp_num: u8) -> Result { let port_state = self.port_states.get(&port_num).ok_or(Error::new(EBADFD))?; -@@ -2398,6 +2715,8 @@ impl Xhci { +@@ -2398,6 +2750,8 @@ impl Xhci { endp_num: u8, clear_feature: bool, ) -> Result<()> { @@ -15487,7 +16394,7 @@ index 5682cf44..ed436619 100644 } } diff --git a/init/src/scheduler.rs b/init/src/scheduler.rs -index d42a4e57..782ca1ba 100644 +index d42a4e57..64e64e1e 100644 --- a/init/src/scheduler.rs +++ b/init/src/scheduler.rs @@ -5,6 +5,7 @@ use crate::unit::{Unit, UnitId, UnitKind, UnitStore}; @@ -15550,32 +16457,51 @@ index d42a4e57..782ca1ba 100644 match &unit.kind { UnitKind::LegacyScript { script } => { for cmd in script.clone() { -@@ -93,7 +104,7 @@ fn run(unit: &mut Unit, config: &mut InitConfig) { +@@ -92,25 +103,30 @@ fn run(unit: &mut Unit, config: &mut InitConfig) { + } UnitKind::Service { service } => { if config.skip_cmd.contains(&service.cmd) { - eprintln!("Skipping '{} {}'", service.cmd, service.args.join(" ")); +- eprintln!("Skipping '{} {}'", service.cmd, service.args.join(" ")); - return; ++ eprintln!("init: skipping {} {}", service.cmd, service.args.join(" ")); + return None; } - if config.log_debug { +- if config.log_debug { ++ eprintln!( ++ "init: starting {} ({})", ++ unit.info.description.as_ref().unwrap_or(&unit.id.0), ++ service.cmd, ++ ); ++ let pid = service.spawn(&config.envs); ++ if pid.is_some() { eprintln!( -@@ -102,7 +113,7 @@ fn run(unit: &mut Unit, config: &mut InitConfig) { - service.cmd, +- "Starting {} ({})", ++ "init: started {} (pid {})", + unit.info.description.as_ref().unwrap_or(&unit.id.0), +- service.cmd, ++ pid.unwrap_or(0), ); } - service.spawn(&config.envs); -+ return service.spawn(&config.envs); ++ return pid; } UnitKind::Target {} => { - if config.log_debug { -@@ -113,4 +124,5 @@ fn run(unit: &mut Unit, config: &mut InitConfig) { - } +- if config.log_debug { +- eprintln!( +- "Reached target {}", +- unit.info.description.as_ref().unwrap_or(&unit.id.0), +- ); +- } ++ eprintln!( ++ "init: reached target {}", ++ unit.info.description.as_ref().unwrap_or(&unit.id.0), ++ ); } } + None } diff --git a/init/src/service.rs b/init/src/service.rs -index ed0023e9..e06e1b16 100644 +index ed0023e9..cc95d02b 100644 --- a/init/src/service.rs +++ b/init/src/service.rs @@ -22,6 +22,8 @@ pub struct Service { @@ -15596,7 +16522,7 @@ index ed0023e9..e06e1b16 100644 let mut command = Command::new(&self.cmd); command.args(self.args.iter().map(|arg| subst_env(arg))); command.env_clear(); -@@ -46,14 +48,20 @@ impl Service { +@@ -46,20 +48,28 @@ impl Service { } command.envs(base_envs).envs(&self.envs); @@ -15619,7 +16545,16 @@ index ed0023e9..e06e1b16 100644 } }; -@@ -81,23 +89,32 @@ impl Service { + match &self.type_ { + ServiceType::Notify => match read_pipe.read_exact(&mut [0]) { +- Ok(()) => {} ++ Ok(()) => { ++ eprintln!("init: {} ready (notify)", self.cmd); ++ } + Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => { + eprintln!("init: {command:?} exited without notifying readiness"); + } +@@ -81,23 +91,34 @@ impl Service { }) => continue, Ok(0) => { eprintln!("init: {command:?} exited without notifying readiness"); @@ -15654,11 +16589,22 @@ index ed0023e9..e06e1b16 100644 + libredox::call::register_scheme_to_ns(current_namespace_fd, scheme, new_fd) + { + eprintln!("init: failed to register scheme {scheme:?} for {command:?}: {err}"); ++ } else { ++ eprintln!("init: {} ready (scheme {})", self.cmd, scheme); + } } ServiceType::Oneshot => { drop(read_pipe); -@@ -112,8 +129,13 @@ impl Service { +@@ -105,6 +126,8 @@ impl Service { + Ok(exit_status) => { + if !exit_status.success() { + eprintln!("init: {command:?} failed with {exit_status}"); ++ } else { ++ eprintln!("init: {} done (oneshot)", self.cmd); + } + } + Err(err) => { +@@ -112,8 +135,13 @@ impl Service { } } } 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 new file mode 100644 index 00000000..0b999755 --- /dev/null +++ b/local/recipes/system/redbear-quirks/source/quirks.d/15-audio.toml @@ -0,0 +1,44 @@ +# Audio controller and codec quirks. +# These apply to HDA controllers and codec devices. + +# Intel ICH8 HDA — force EAPD on outputs +[[pci_quirk]] +vendor = 0x8086 +device = 0x284b +flags = ["audio_force_eapd"] + +# Intel ICH9 HDA (QEMU) — use immediate command interface +[[pci_quirk]] +vendor = 0x8086 +device = 0x293e +flags = ["audio_single_cmd"] + +# Intel 6-series PCH HDA — position fix LPIB +[[pci_quirk]] +vendor = 0x8086 +device = 0x1c20 +flags = ["audio_position_fix_lpib"] + +# Intel 7-series PCH HDA — position fix LPIB +[[pci_quirk]] +vendor = 0x8086 +device = 0x1e20 +flags = ["audio_position_fix_lpib"] + +# Intel Sunrise Point HDA — position fix LPIB +[[pci_quirk]] +vendor = 0x8086 +device = 0xa170 +flags = ["audio_position_fix_lpib"] + +# Intel Cannon Point HDA — position fix LPIB +[[pci_quirk]] +vendor = 0x8086 +device = 0x9dc8 +flags = ["audio_position_fix_lpib"] + +# AMD FCH HDA — single command fallback +[[pci_quirk]] +vendor = 0x1022 +device = 0x1457 +flags = ["audio_single_cmd"]