docs: Linux-aligned input-stack consolidation plan

Cross-referenced the console-input path against Linux/CachyOS
(local/reference/linux-7.1/drivers/{input,tty}/). Linux applies the
keymap once (vt/keyboard.c) and runs one line discipline (n_tty.c)
shared by the console tty and ptys; RedBear duplicates the keymap
(inputd + fbcond) and CR/LF (fbcond + ptyd) and runs two disciplines
bridged by getty — which is where typed characters are lost (echoed but
not executed). Plan: inputd owns the keymap and emits the full byte
stream (incl. escape sequences); fbcond becomes display-only passthrough
(no keymap, no CR/LF, no cooking); ptyd is the sole line discipline.
Staged, each stage independently bootable; validation needs the build
green (acpi-rs currently blocks it).
This commit is contained in:
2026-07-24 11:04:01 +09:00
parent 491659b1b6
commit 78aca1bb45
@@ -0,0 +1,105 @@
# Input Stack — Linux-Aligned Consolidation Plan
**Status:** Design (implementation staged below). Goal: make the console-input path match the
Linux/CachyOS model — one keymap owner, one line discipline shared by console and pty — instead of
the current duplicated, two-discipline chain that loses typed characters (echoed but not executed).
CachyOS's input/tty is stock upstream Linux (the `local/reference/cachyos/` tree is packaging +
kernel *performance* patches; the input subsystem is unmodified). So the reference model below is
`local/reference/linux-7.1/drivers/{input,tty}/`.
## The Linux model (single owner per stage)
| Stage | Linux file | Property |
|---|---|---|
| Raw input pipeline | `drivers/input/input.c` (`input_pass_values`) | one driver→core→handler pipeline |
| **Keymap → bytes** | `drivers/tty/vt/keyboard.c` (`put_queue`, `to_utf8`, `applkey`) | **applied ONCE**; emits the full byte stream incl. escape sequences for special keys |
| **Line discipline** | `drivers/tty/n_tty.c` (`read_buf` cooked, `echo_buf`, ICRNL, ISIG) | **ONE module**: canonical, echo, cooked buffer, CR/LF, signal keys |
| Console vs pty | `drivers/tty/pty.c` (`tty_port`, shared ldisc) | pty is a thin bridge that **reuses `n_tty`** — no second discipline |
Key invariants Linux guarantees and we currently violate:
1. The keymap is applied **exactly once**.
2. CR/LF (`ICRNL`/`ONLCR`) is one `termios` policy in **one** place.
3. Canonical/echo/cooked lives in **one** buffer pair, and the **console tty and ptys share the
same line discipline**. "Echoed but not delivered" is structurally impossible because the reader
drains the very buffer the discipline cooked into.
## Current RedBear path (what to fix)
```
ps2d ─▶ inputd ─▶ fbcond ─▶ getty(bridge) ─▶ ptyd ─▶ pty-slave ─▶ shell
keymap keymap+ byte pump FULL line discipline
(get_char) CR→LF (canonical/echo/cooked/CR→LF/signals)
+ own input
queue
```
Confirmed wiring (`local/sources/userutils/src/bin/getty.rs`):
- getty opens `/scheme/fbcon/{vt}` (fbcond) **and** `/scheme/pty/ptmx` (ptyd), then byte-pumps
console↔pty; login/shell stdin = pty **slave**.
Duplication:
- **Keymap twice**: inputd `active_keymap.get_char` (`drivers/inputd/src/main.rs:452`) **and** fbcond
`self.keymap.lookup` (`drivers/graphics/fbcond/src/text.rs:140`).
- **CR→LF twice**: fbcond (`text.rs:164`) **and** ptyd (`ICRNL`, `drivers/ptyd/src/pty.rs:76`).
- **Two disciplines**: fbcond has its own input queue/cooking; ptyd has the real one. The bridge
between them is where typed characters are lost while bare `\n` survives (the reported bug:
`tlc` echoes but never executes; manual Enters produce empty prompts).
## Target (Linux-aligned)
```
ps2d ─▶ inputd ─▶ fbcond(display only) ─▶ getty(dumb pipe) ─▶ ptyd ─▶ slave ─▶ shell
keymap→ passthrough of the THE single line
BYTES already-decoded byte discipline (n_tty)
(incl. stream; renders output;
escapes) NO keymap, NO CR/LF,
NO input cooking
```
Rules:
1. **inputd owns the keymap** and emits the **full byte stream** for each key, including escape
sequences for special keys (arrows, F-keys, Home/End) — the `keyboard.c`/`applkey` role. Today it
only sets a single `character`; extend it to emit bytes.
2. **fbcond is display-only + a raw byte passthrough.** Remove `self.keymap.lookup` and the
`text.rs:164` CR→LF. It forwards inputd's decoded bytes verbatim into the console read stream and
renders output. It performs **no** cooking, echo, or CR/LF.
3. **ptyd is the sole line discipline** (the `n_tty`). Canonical/echo/cooked/CR→LF/signals stay here
and **only** here. This is already implemented (`pty.rs:41-249`); it just becomes the single owner.
4. **getty stays a dumb byte pipe** (no processing) — matches a terminal emulator feeding a pty.
Result: keymap once (inputd), CR/LF once (ptyd `termios`), one cooked buffer (ptyd) that the shell
drains directly. Structurally identical to Linux → the "echoed but not delivered" class is gone.
## Staged implementation (each stage builds + boots independently; validate on an idle host)
1. **inputd: emit full byte sequences.** Add a keymap `get_bytes(scancode, mods) -> &[u8]` that
returns the char OR the escape sequence for special keys (model on `keyboard.c`). Deliver bytes on
the consumer stream (or keep events but carry the full byte payload). Keep behavior identical for
plain chars first; add escapes next.
2. **fbcond: drop keymap + CR/LF, become passthrough.** Remove `self.keymap.lookup` and the CR→LF at
`text.rs:164`; forward inputd's bytes verbatim. fbcond keeps only rendering + scrollback + the
serial mirror (which should also stop translating CR/LF — leave that to ptyd).
3. **Verify one discipline.** Confirm ptyd is the only place doing canonical/echo/cooked/CR→LF; delete
fbcond's residual input-cooking paths (`push_input_bytes` CR→LF at `pty.rs`-style sites in fbcond).
4. **Regression pass.** Console login on framebuffer AND serial: type a command, confirm execution;
test backspace, Ctrl-C (ISIG), arrows (escape sequences), and the serial mirror.
Immediate stopgap (independent of this refactor, already partly done): the brush minimal backend
reads raw and accumulates a persistent line buffer; extend it to treat **both** `\r` and `\n` as
submit so the shell is robust even before the stack is consolidated. This is a shell-side band-aid,
not the fix — the fix is stages 13.
## Validation
- Framebuffer screendump (QMP) is ground truth. **One boot proves nothing** (input races); compare
across boots on an idle host — external load (opencode) contaminates timing.
- Success: type `echo RB=$((21*2))=OK` on the framebuffer console → `RB=42=OK` executes; backspace,
Ctrl-C, and arrows behave; serial-console login works the same way.
## Risk / rollback
- Boot-critical path; each stage is independently bootable and revertable (one component per stage).
- Do not land any stage validated only under external load.
- Build is currently blocked on `acpi-rs` (`aml/mod.rs:258` `connect_op_regions` undefined) — none of
this can be verified until that compiles.