docs(v4): the stick, increment 1 plan: v4's screen and keyboard

Approved by Captain Bob 2026-10-26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-26 13:47:11 -04:00
co-authored by Claude Opus 5.5
parent 1a54f9ef59
commit 6f96502fdf
@@ -0,0 +1,125 @@
# The stick, increment 1: v4's screen and keyboard — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans (Captain Bob prefers native execution). Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** On the v4 path the screen is dark until Zuse logs in and is then the console, and the PC's
keyboards (PS/2, USB, and virtio on aarch64/riscv64) type to it as they do to v3's console.
**Architecture:** v3's keyboard bridge (`repl.c`'s key tables, lock state and per-tick USB servicing)
moves into a HAL file both paths call, unchanged in behaviour. The console gains a switch for whether
text is mirrored to the framebuffer; the v4 boot brings the framebuffer up with the mirror off, turns it
on (and TTF, and the cursor) when Zuse logs in, and turns it off and clears the screen when she logs out.
The splash itself is increment 7; until then "splash only" is a dark screen.
**Tech Stack:** freestanding C99 kernel (`kernel/`), QEMU (OVMF/AAVMF/OpenSBI), QMP (`send-key`,
`input-send-event`, `screendump`), Python 3 for the session harness.
**Spec:** `docs/v4.0.0/STICK.md` §2, §5.3, §5.4, §9, §10, §11 increment 1. The identity gate:
`docs/v4.0.0/plans/2026-10-26-engine-step4-identity-gate.md` and its final review.
## Global Constraints
- Branch `StarForth-v4.0.0`, main checkout; no branch, no stash, never `--no-verify`. Commit with the
LFS filter off (`git -c filter.lfs.process= -c filter.lfs.clean=cat -c filter.lfs.smudge=cat -c
filter.lfs.required=false`); push with GitKraken's git-lfs on `PATH`. Messages end
`Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
- Strict C99, `-Wall -Wextra -Werror` (`.claude/CLAUDE.md` Code Standards).
- "Splash only, log on serial": before Zuse logs in, nothing the console prints reaches the screen.
- v3's behaviour does not change (STICK.md §10); shown by a `system=v3` boot typed to by keyboard.
- "QEMU and bare metal ALWAYS hold parity": the keyboards and display QEMU gives are the same drivers
a PC uses; no QEMU-only input path.
- Never apply a fix that is not in this plan; report it.
- Suite per task: `make -C v4 test test-32 sanitize hosted-check` (nothing in `v4/` changes; it must
still pass). Full acceptance on every kernel-affecting task (`.claude/CLAUDE.md`): three `clean qemu`
boots to `[zuse@Hera] ok> `, POST 538/538, equal `dict_hash` (`0xfec8c47cfc6f437f` unless a task
says otherwise), `make -C v4 test-32` 0 failures; logs committed.
## Review Focus
1. **Keys pressed on a keyboard while headless** are dropped, as serial bytes are (the gate's review
item 4): `sk_v4_identity_wait` discards keyboard events too. Pinned in Task 2 (`kbd_typeahead`).
2. **Logging out leaves nothing of the session on screen:** the screen is cleared when Zuse's session
ends. Pinned in Task 2 (`screen_dark_after_logout`).
3. **A PC with no framebuffer** (GOP gave none): v4 runs on serial alone and does not fault. Pinned in
Task 2 (`no_framebuffer`, QEMU with `-vga none` on amd64).
4. **Alt+Tab** toggles the graphics/text view on v4 as on v3 (the bridge's own interception moves with
it). Covered by Task 1's move being byte-for-byte; checked by eye in Task 2's screendumps.
5. **v3's console** still takes keys from every keyboard: Task 1's yardstick, a `system=v3` boot typed
to with `send-key`.
---
### Task 1: The keyboard bridge, shared
**Files:**
- Create: `kernel/src/hal/kbd_input.c`, `kernel/include/starkernel/kbd_input.h`
- Modify: `kernel/src/repl.c` (remove the moved code; `sk_console_getc_raw` calls the new function)
- Create: `scripts/qemu_session.py` (the session harness, kept in the repo from here on)
**Interfaces:**
- Produces: `int sk_input_getc(void);` — one raw byte from the serial line or a keyboard
(PS/2, USB boot keyboard, virtio), non-blocking, -1 when none; services the xHCI event ring once
per timer tick while a USB keyboard runs; Alt+Tab toggles the graphics view and returns nothing.
Exactly `repl.c`'s `sk_console_getc_raw()` today, with `sk_kbd_getc()`, its tables, the
Shift/Alt/Caps Lock/Num Lock state and `sk_kbd_show_locks()` moved with it.
- Produces: `void sk_input_drop(void);` — read and drop every pending byte and key event (for
Task 2's headless wait).
- Produces: `scripts/qemu_session.py MODE ARCH START_EPOCH [ARGS]` — today's scratch `gate_test.py`
(modes `headless`, `zuse`, `hotplug`, `midpull`, `waitpull`, `typeahead`, `emergency`) plus the
modes added below; it prints `PASS:`/`FAIL:` lines and `RESULT: PASS|FAIL`, and quits QEMU over QMP.
- [ ] **Step 1: Test (the yardstick, before):** add mode `v3_keys` to the harness: boot
`system=v3` with the default disks, wait for `[zuse@Hera] ok> `, type `3 4 + .` with QMP `send-key`
(one `send-key` per character, `qcode`s from QEMU's list; Enter `ret`), and check `7 ok` follows.
Run on amd64 (PS/2) and riscv64 (virtio keyboard). Expected: PASS both (this is the reference).
- [ ] **Step 2:** Move the code; `repl.c` keeps `g_console_pending_key` and calls `sk_input_getc()`.
- [ ] **Step 3:** `v3_keys` again on amd64 and riscv64. Expected: PASS, the same lines.
- [ ] **Step 4:** Suite; commit `refactor(kernel): the keyboard bridge, shared` with the four logs
and `scripts/qemu_session.py`.
### Task 2: v4's screen and keyboard
**Files:**
- Modify: `kernel/src/hal/console.c`, `kernel/include/starkernel/console.h`
- Modify: `kernel/src/v4/sk_v4.c` (the line editor reads `sk_input_getc()`; cursor),
`kernel/src/v4/sk_v4_identity.c` (framebuffer and keyboards brought up; mirror on at login, off
and cleared at logout; `sk_input_drop()` in the wait), `kernel/src/kernel_main.c` (the v4 branch
passes the framebuffer: `sk_v4_boot_info` already holds it)
**Interfaces:**
- Consumes: Task 1's `sk_input_getc()`, `sk_input_drop()`.
- Produces: `void console_fb_mirror(int on);` — whether `console_putc` also draws on the
framebuffer; on by default, so v3 is unchanged. `void console_fb_clear(void);` — erase the
screen and home the cursor (`vt100_puts("\x1b[2J\x1b[H")` when the framebuffer is up).
- Bring-up order on the v4 path (in `sk_v4_identity_boot`, before xHCI): `console_fb_init` with the
pixel format chosen as `kernel_main.c`'s v3 path does, then `console_fb_mirror(0)`;
`virtio_input_find_keyboard()` (as the v3 path does; a no-op where there is none).
- At login (where `sk_v4_drive_arrived` starts her session): `console_fb_mirror(1)`,
`console_fb_enable_ttf()`. At logout (`sk_v4_drive_left`): `console_fb_mirror(0)`,
`console_fb_clear()`.
- The v4 line editor: `sk_input_getc()` in place of `console_getc()`; the cursor shown at the prompt
and after each echo or rub-out (`console_fb_draw_cursor`), and blinking every 50 ticks while idle,
as `sk_console_readline` does.
- [ ] **Step 1: Failing tests** (harness modes; amd64 unless named):
- `screen_dark_until_login`: no Zuse drive; 30 s after `POST: PASSED`, QMP `screendump` (PPM);
every pixel black. Today: FAIL (the loader's text is still on screen).
- `screen_console_after_login`: hot-add Zuse; after `[zuse@Hera] ok> `, a screendump has at least
500 non-black pixels; type `3 4 + .` by `send-key`; `7 ok` on serial and a later screendump
differs from the first. Today: FAIL.
- `screen_dark_after_logout`: then `device_del` her drive; after "Zuse: session ended", a
screendump is all black. Today: FAIL.
- `kbd_typeahead`: no drive; `send-key` `3 4 + .` while headless; hot-add Zuse; no `7 ok`;
then `1 2 + .` by `send-key` gives `3 ok`. Today: FAIL (keys are not read at all).
- `usb_kbd`: `QEMU_EXTRA="-device usb-kbd,bus=xhci0.0,port=3,id=ukbd"`; after login, type
`3 4 + .` with `input-send-event` (`device: ukbd`); `7 ok`. Today: FAIL.
- `no_framebuffer`: amd64 with `-vga none`; the default disks; `[zuse@Hera] ok> ` and `3 4 + .`
over serial gives `7 ok`. Today: PASS (keep it passing).
- `virtio_kbd`: riscv64 and aarch64, after login, `send-key` `3 4 + .` gives `7 ok`. Today: FAIL.
- [ ] **Step 2:** Run each; record the failures.
- [ ] **Step 3:** Build Task 2 as above.
- [ ] **Step 4:** Every mode passes; the gate's sessions still pass (`headless`, `zuse`, `hotplug`,
`midpull`, `waitpull`, `typeahead`, `emergency`); `v3_keys` still passes.
- [ ] **Step 5:** Suite; full acceptance; commit `feat(v4): the screen and the keyboards` and the logs.
Record in the commit message whether `dict_hash` changed (it should not: nothing on the node
changes).