Files
RedBear-OS/local/docs/INPUT-SCHEME-ENHANCEMENT.md
T

544 lines
17 KiB
Markdown

# 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 confirm the limitation:
- `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.
## 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:
```text
/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:
```text
/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:
```rust
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 }`
- `events``Handle::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:
```rust
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
```text
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
```rust
#[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:
```rust
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:
```text
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
```rust
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
```rust
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:
```rust
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:
```rust
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`
`ps2d` is the first caller that should adopt the new API because it already has a clean split between keyboard and mouse sources.
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()`
Routing:
- keyboard scancodes → `ps2-keyboard`
- mouse move / absolute move / button / scroll events → `ps2-mouse`
This preserves compatibility with old `inputd` while immediately 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`
`usbhidd` can remain legacy initially, then later migrate to named producers such as `usb-hid0`, `usb-hid1`, or more specific per-interface names.
## 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
## 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.