Files
RedBear-OS/local/docs/INPUT-SCHEME-ENHANCEMENT.md
T
vasilito 08bea46575 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.
2026-04-24 20:25:00 +01:00

18 KiB

INPUTD SCHEME API ENHANCEMENT DESIGN

Target: recipes/core/base/source/drivers/inputd Scope: Userspace-only inputd scheme enhancement Date: 2026-04-13

1. Goal

Enhance inputd so it can do all of the following without breaking any existing callers:

  1. Let producers register under stable names such as ps2-keyboard, ps2-mouse, or usb-hid0.
  2. Expose per-device consumer streams so services such as evdevd can subscribe to one device only.
  3. Publish hotplug notifications for device add/remove.
  4. Expose currently registered devices through the scheme root directory.

This is an additive design. Existing paths, existing event payloads, existing VT behavior, and existing display/control behavior must continue to work unchanged.

2. Current Implementation Summary

The current inputd implementation in recipes/core/base/source/drivers/inputd/src/main.rs has these important properties:

  • Handle only supports Producer, Consumer, Display, Control, and SchemeRoot.
  • openat() only recognizes producer, consumer, consumer_bootlog, handle, handle_early, and control.
  • All producers write anonymous orbclient::Event bytes into the same Handle::Producer path.
  • Legacy consumers are per-VT handles. write() only delivers input bytes to the active VT consumer set.
  • 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 after migration:

  • 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

  1. Keep legacy behavior intact: /scheme/input/producer and /scheme/input/consumer must keep working exactly as they do today.
  2. Do not change event payloads: device-specific streams still carry serialized orbclient::Event values.
  3. Keep all logic in userspace: no kernel changes, no new kernel scheme semantics.
  4. Make enumeration path-driven: device names are visible as entries below /scheme/input/.
  5. Use explicit hotplug events: device discovery and liveness must not depend on polling failed opens.

4. Scheme Path Layout

The enhanced namespace is:

/scheme/input/                    — SchemeRoot (directory listing)
/scheme/input/producer            — Legacy producer (unchanged)
/scheme/input/producer/{name}     — Named producer: ps2-keyboard, ps2-mouse, usb-hid0
/scheme/input/consumer            — Legacy consumer (unchanged)
/scheme/input/{device_name}       — Per-device consumer: reads events from one named producer
/scheme/input/events              — Hotplug event stream
/scheme/input/handle/{display}    — Display handle (unchanged)
/scheme/input/control             — Control commands (unchanged)

Legacy-only paths that must remain valid even though they are not part of the new API surface:

/scheme/input/consumer_bootlog    — Existing bootlog VT consumer
/scheme/input/handle_early/{display} — Existing early framebuffer handoff path

4.1 Root Directory Listing

SchemeRoot should become a real directory endpoint backed by getdents, not by overloading read().

The root listing should expose:

  • static entries: producer, consumer, consumer_bootlog, events, handle, handle_early, control
  • one dynamic entry per registered device name from devices

That keeps the namespace honest while still allowing device enumeration from /scheme/input/.

InputDeviceLister in lib.rs should filter out the reserved static names and return only dynamic device entries.

5. Handle Model

The Handle enum in main.rs should become:

enum Handle {
    Producer,
    NamedProducer {
        name: String,
    },
    Consumer {
        events: EventFlags,
        pending: Vec<u8>,
        needs_handoff: bool,
        notified: bool,
        vt: usize,
    },
    DeviceConsumer {
        device_name: String,
        events: EventFlags,
        pending: Vec<u8>,
        notified: bool,
    },
    HotplugEvents {
        events: EventFlags,
        pending: Vec<u8>,
        notified: bool,
    },
    Display {
        events: EventFlags,
        pending: Vec<VtEvent>,
        notified: bool,
        device: String,
        is_earlyfb: bool,
    },
    Control,
    SchemeRoot,
}

Notes:

  • Producer remains the legacy anonymous producer path.
  • NamedProducer only needs the registered name. Device ID lookup stays in shared scheme state.
  • DeviceConsumer is byte-oriented like the legacy consumer, but without VT or handoff state.
  • HotplugEvents stores serialized variable-length hotplug records in pending.
  • SchemeRoot remains a dedicated handle variant, but now supports directory enumeration.

6. Scheme Open Semantics

openat() should parse paths as follows:

6.1 Existing Paths

  • producer with no child component → Handle::Producer
  • consumer → current VT consumer allocation logic
  • consumer_bootlog → current VT 1 logic
  • handle/{display} → unchanged
  • handle_early/{display} → unchanged
  • control → unchanged

6.2 New Paths

  • producer/{name}Handle::NamedProducer { name }
  • eventsHandle::HotplugEvents { ... }
  • any other top-level non-reserved path component → Handle::DeviceConsumer { device_name, ... }

6.3 Name Validation

Named producer registration must reject:

  • empty names
  • names containing /
  • reserved names: producer, consumer, consumer_bootlog, events, handle, handle_early, control
  • duplicate live names already present in devices

Recommended error behavior:

  • invalid name → EINVAL
  • duplicate name → EEXIST
  • open of /scheme/input/{device_name} for a currently unknown device → ENOENT

7. State Management

InputScheme should add:

devices: BTreeMap<String, u32>,
next_device_id: AtomicUsize,

Purpose:

  • devices maps device name → current device ID
  • next_device_id allocates monotonically increasing IDs

Behavior:

  1. When NamedProducer opens successfully:

    • allocate device_id = next_device_id.fetch_add(1, Ordering::SeqCst) as u32
    • insert devices.insert(name.clone(), device_id)
    • serialize a DEVICE_ADD hotplug message
    • append it to every Handle::HotplugEvents.pending
    • set notified = false on those hotplug handles
    • set has_new_events = true
  2. When NamedProducer closes:

    • remove the entry from devices
    • serialize a DEVICE_REMOVE hotplug message with the removed ID and name
    • append it to every Handle::HotplugEvents.pending
    • set notified = false
    • set has_new_events = true
  3. Device IDs are never reused. If ps2-keyboard disappears and later comes back, it gets a new device_id.

No additional kernel state is required. This is ordinary daemon-side bookkeeping.

8. Event Routing Logic

The existing preprocessing path in write() must remain in place:

  • special Super+Fn VT switching behavior stays in inputd
  • keymap translation still happens in inputd
  • the emitted payload remains serialized orbclient::Event

After that preprocessing step, routing changes as follows.

8.1 Legacy Producer

Input written to /scheme/input/producer follows the current legacy route:

  • deliver to the existing legacy consumer path
  • preserve current active-VT behavior
  • do not deliver to any DeviceConsumer
  • do not generate hotplug events

8.2 Named Producer

Input written to /scheme/input/producer/{name} must be fanned out to:

  1. the matching DeviceConsumer handles where device_name == name
  2. the existing legacy consumer path used by older display/input clients

That means named producers are supersets of legacy routing, not replacements.

8.3 Device Consumer

/scheme/input/{device_name} only receives events from the named producer with the exact same name.

It must never receive:

  • anonymous legacy producer traffic
  • events from other named producers
  • display or control events

8.4 Routing Sketch

legacy producer write
  -> existing input normalization
  -> legacy VT consumer fan-out only

named producer write(name)
  -> existing input normalization
  -> device consumers for name
  -> legacy VT consumer fan-out

Implementation-wise, the simplest approach is:

  1. detect whether the writer is Producer or NamedProducer { name }
  2. run the existing event transformation code once
  3. serialize transformed Event values once
  4. if named, append to matching DeviceConsumer.pending
  5. append to the legacy consumer path using the current active-VT logic
  6. clear notified on affected readers and set has_new_events = true

9. Hotplug Event Stream

/scheme/input/events is a read-only stream of variable-length hotplug records.

9.1 Binary Format

#[repr(C)]
struct InputHotplugEvent {
    kind: u32,       // 1 = DEVICE_ADD, 2 = DEVICE_REMOVE
    device_id: u32,  // Unique device identifier
    name_len: u32,   // Length of device name
    _reserved: u32,  // Future use
}
// Followed by name_len bytes of UTF-8 device name

Constants:

const DEVICE_ADD: u32 = 1;
const DEVICE_REMOVE: u32 = 2;

9.2 Stream Semantics

  • The stream is append-only and ordered by daemon observation.
  • Each record is serialized as header bytes followed by UTF-8 name bytes.
  • read() drains raw bytes from pending.
  • Because records are variable-length, callers must handle partial reads.
  • HotplugHandle in lib.rs should hide this by buffering partial bytes until one full record is available.

9.3 Notification Model

Handle::HotplugEvents participates in fevent(EVENT_READ) exactly like other readable handles:

  • when at least one serialized hotplug record is pending and the handle is subscribed to EVENT_READ, post a read event
  • after a successful read drains the buffer, notification becomes edge-triggered again

10. Scheme Root Enumeration

Enumeration should be implemented with getdents() on Handle::SchemeRoot.

Recommended behavior:

  • scheme_root() still creates a Handle::SchemeRoot
  • getdents() emits static entries plus one entry per devices key
  • read() on SchemeRoot stays invalid (EBADF or EISDIR are both acceptable if applied consistently)
  • openat() continues to require a valid SchemeRoot dirfd

Example visible entries after ps2d registers keyboard and mouse:

producer
consumer
consumer_bootlog
events
handle
handle_early
control
ps2-keyboard
ps2-mouse

This gives normal filesystem-style discovery while keeping old endpoints visible.

11. lib.rs Public API Changes

The public API should be extended, not replaced.

11.1 Existing Types Stay

  • ProducerHandle
  • ConsumerHandle
  • DisplayHandle
  • ControlHandle

Their existing constructors and behavior remain unchanged.

11.2 New Types

pub struct NamedProducerHandle(File);
pub struct DeviceConsumerHandle(File);
pub struct HotplugHandle {
    file: File,
    partial: Vec<u8>,
}

#[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 InputDeviceLister;

11.3 Constructors

impl NamedProducerHandle {
    pub fn new(name: &str) -> io::Result<Self>;
}

impl DeviceConsumerHandle {
    pub fn new(device_name: &str) -> io::Result<Self>;
}

impl HotplugHandle {
    pub fn new() -> io::Result<Self>;
}

Path mapping:

  • NamedProducerHandle::new("ps2-keyboard")/scheme/input/producer/ps2-keyboard
  • DeviceConsumerHandle::new("ps2-keyboard")/scheme/input/ps2-keyboard
  • HotplugHandle::new()/scheme/input/events

11.4 Read/Write Shape

Recommended API shape:

impl NamedProducerHandle {
    pub fn write_event(&mut self, event: orbclient::Event) -> io::Result<()>;
}

pub enum DeviceConsumerHandleEvent<'a> {
    Events(&'a [Event]),
}

impl DeviceConsumerHandle {
    pub fn event_handle(&self) -> BorrowedFd<'_>;
    pub fn read_events<'a>(&self, events: &'a mut [Event])
        -> io::Result<DeviceConsumerHandleEvent<'a>>;
}

impl HotplugHandle {
    pub fn event_handle(&self) -> BorrowedFd<'_>;
    pub fn read_event(&mut self) -> io::Result<Option<HotplugEvent>>;
}

DeviceConsumerHandle deliberately mirrors ConsumerHandle, but it does not need Handoff support because VT display handoff is unrelated to per-device streams.

11.5 Device Enumeration Helper

InputDeviceLister should provide a safe wrapper around scheme-root directory reads, for example:

impl InputDeviceLister {
    pub fn list() -> io::Result<Vec<String>>;
}

Behavior:

  • read /scheme/input/ as a directory
  • drop reserved static entries
  • return only currently registered device names

This keeps callers out of scheme-internal filtering logic.

12. Producer Lifecycle and Consumer Behavior

12.1 Named Producer Registration

Opening /scheme/input/producer/{name} is both:

  • creation of a producer handle
  • registration of {name} as a live device

Closing the fd unregisters the device.

This matches current scheme style well because inputd already uses on_close() to clean up VT consumers.

12.2 Device Consumer Lifetime

Per-device consumer handles are name-based subscriptions.

  • open succeeds only while the device name is currently registered
  • once open, the handle remains attached to that name
  • if the producer disappears, no more events arrive for that handle
  • if the same name is registered again later, the handle resumes receiving events for that name
  • the hotplug stream is how clients notice that the underlying producer instance changed

This keeps DeviceConsumer simple and avoids introducing a second handle teardown protocol.

13. Migration Path

13.1 ps2d — MIGRATED

ps2d now uses two InputProducer instances with named-first, legacy-fallback strategy:

  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 → self.keyboard_input
  • mouse move / absolute move / button / scroll events → self.mouse_input

This preserves compatibility with old inputd while enabling per-device consumers on new inputd.

13.2 evdevd

Once the scheme exists, local evdevd can move from /scheme/input/consumer to:

  • InputDeviceLister::list() to discover devices
  • DeviceConsumerHandle::new(name) for device-local streams
  • HotplugHandle::new() to watch add/remove

It can keep the legacy consumer path as a fallback for older systems.

13.3 usbhidd — MIGRATED

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

All of the following must continue to work unchanged:

  • /scheme/input/producer
  • /scheme/input/consumer
  • /scheme/input/consumer_bootlog
  • /scheme/input/handle/{display}
  • /scheme/input/handle_early/{display}
  • /scheme/input/control
  • current ProducerHandle, ConsumerHandle, DisplayHandle, and ControlHandle APIs
  • current active-VT routing and graphics handoff behavior

Compatibility rules:

  1. Old producers still emit anonymous events into the legacy stream.
  2. Old consumers still receive the same event format and VT behavior.
  3. New named producers additionally feed the legacy stream, so old consumers continue to see those events.
  4. No caller is forced to understand hotplug or enumeration.

15. Non-Goals

This design does not include:

  • capability discovery (keyboard vs mouse metadata)
  • kernel support or syscall ABI changes
  • replacing orbclient::Event with a new event format
  • changing VT ownership, display handoff, or control command semantics
  • automatic migration of existing daemons

16. Implementation Checklist

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
  11. migrate usbhidd with a named-producer-first, legacy-fallback strategy

17. Final Outcome

After this enhancement:

  • Legacy consumers continue to work as-is.
  • ps2d and future drivers can publish stable device names.
  • evdevd and similar services can subscribe to exactly one device stream.
  • userspace can enumerate live input devices and react to hotplug events.

That solves the current anonymity problem without changing the kernel or breaking the existing Redox input stack.