From febfd21e899a199c2108187000d7990086cac544 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 07:06:06 +0200 Subject: [PATCH 1/8] ps2server: the i8042 driver leaves the kernel for userland The kernel keeps the `isa` claim on the i8042's ports and lines and speaks none of its protocol. /system/bin/ps2server probes the controller after IBM's PS/2 Hardware Interface Technical Reference (84F9735) and the TrackPoint System Version 4.0 Engineering Specification: the keyboard is reset at every start and armed set 2 translated, every command byte holds the System Flag and the keyboard interface disabled until the arming's 0xAE, a byte is read 7 us after the status read that showed it and written only with both buffers empty, and a device's answers wait on its interrupt. The stream feeds the pointer's framer the device's own idle time, so a slow reader never splits a packet. ps2server hands each key and pointer report to the owner of input over the new `input` port, which only it may receive. The owner (the compositor, or the console on its image) merges every source with toyos-input: a key or a button is held while any source holds it, and a source's end releases what it held and nothing another holds. The kernel's merge goes: its keyboard and mouse streams carry its USB sources' records unmerged, and MouseEvent carries what a device said - its source, its own buttons, scroll, and relative or absolute motion. The supervisor waits on every start of a row that serves a port or is `restart`, and starts a `restart` row again only when it ended other than by exit 0 (toyos-manifest's Program::supervised, with its module test). Deleted: the kernel's i8042 driver and tally, its vector, the keyboard_controller seam, irq_ring's and the census's i8042 source, the i8042-withheld and test-input-merge actuators and the input-merge guest row, the loom tally model, iapc_boot_arch, isa_claim_refused (its premise, a kernel driving the controller, is gone), and issues/t14-hands-over-an-uninitialised-8042.md, whose fallback from firmware's command byte the reset at every start deletes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- Cargo.lock | 25 +- Cargo.toml | 7 +- build/build.rs | 23 +- images/console.toml | 17 +- images/diag.toml | 3 +- images/system.toml | 17 +- .../t14-hands-over-an-uninitialised-8042.md | 46 - ...-small-interrupts-post-and-threads-wait.md | 42 +- kernel/Cargo.toml | 1 - kernel/loom/src/lib.rs | 16 +- kernel/loom/tests/i8042_tally.rs | 214 --- kernel/pci/Cargo.toml | 4 +- kernel/src/actuator.rs | 8 - .../src/arch/aarch64/keyboard_controller.rs | 13 - kernel/src/arch/aarch64/mod.rs | 1 - kernel/src/arch/x86_64/boot.rs | 1 - kernel/src/arch/x86_64/i8042/mod.rs | 1284 ----------------- kernel/src/arch/x86_64/i8042/tally.rs | 80 - kernel/src/arch/x86_64/idt/i8042.rs | 6 - kernel/src/arch/x86_64/idt/mod.rs | 5 - kernel/src/arch/x86_64/mod.rs | 8 +- kernel/src/device.rs | 10 - kernel/src/drivers/acpi.rs | 6 - kernel/src/drivers/xhci/device.rs | 2 +- kernel/src/drivers/xhci/hid.rs | 14 +- kernel/src/drivers/xhci/wait/boot.rs | 3 - kernel/src/input_merge_test.rs | 155 -- kernel/src/irq_census.rs | 6 +- kernel/src/irq_ring.rs | 4 +- kernel/src/keyboard.rs | 94 +- kernel/src/main.rs | 8 - kernel/src/mouse.rs | 152 +- kernel/src/sched/driver.rs | 3 - kernel/usbhid/Cargo.toml | 4 + kernel/usbhid/src/keyboard.rs | 94 +- lib/acpi/src/fadt.rs | 12 - lib/acpi/src/lib.rs | 2 +- lib/acpi/tests/corpus.rs | 20 +- lib/acpi/tests/fixtures.rs | 8 +- lib/elf/Cargo.toml | 4 +- lib/fat32/Cargo.toml | 2 +- lib/gpt/Cargo.toml | 2 +- lib/hda/Cargo.toml | 4 +- lib/input/Cargo.toml | 13 + lib/input/src/keys.rs | 134 ++ lib/input/src/lib.rs | 23 + lib/input/src/merge.rs | 166 +++ lib/input/src/wire.rs | 155 ++ lib/input/tests/input.rs | 177 +++ lib/manifest/src/lib.rs | 62 + sdk/abi/src/input.rs | 48 +- sdk/keymap/Cargo.toml | 2 +- sdk/toyos/src/endow.rs | 8 + system/compositor/Cargo.toml | 1 + system/compositor/desktop/Cargo.toml | 1 + system/compositor/desktop/src/input.rs | 79 +- system/compositor/desktop/src/lib.rs | 2 +- system/compositor/src/main.rs | 8 +- system/compositor/src/session.rs | 277 +++- system/console/Cargo.toml | 1 + system/console/src/main.rs | 146 +- system/ps2server/Cargo.toml | 17 + {kernel => system/ps2server}/ps2/Cargo.toml | 5 +- {kernel => system/ps2server}/ps2/src/key.rs | 20 +- {kernel => system/ps2server}/ps2/src/lib.rs | 6 +- {kernel => system/ps2server}/ps2/src/mouse.rs | 2 +- .../ps2server}/ps2/tests/decode.rs | 0 .../ps2server}/ps2/tests/fuzz.rs | 0 system/ps2server/src/bus.rs | 142 ++ system/ps2server/src/fake.rs | 467 ++++++ system/ps2server/src/hw.rs | 90 ++ system/ps2server/src/main.rs | 90 ++ system/ps2server/src/probe.rs | 515 +++++++ system/ps2server/src/stream.rs | 440 ++++++ system/supervisor/src/main.rs | 15 +- tests/checks.rs | 4 +- tests/common/irqcensus.rs | 8 +- tests/common/isa.rs | 15 +- tests/common/mod.rs | 2 + tests/common/ps2server.rs | 42 + .../guest/rust-tests/src/bin/acpi_mediated.rs | 9 +- .../rust-tests/src/bin/isa_claim_refused.rs | 23 - tests/guest/rust-tests/src/bin/isa_grant.rs | 3 +- tests/guest/rust-tests/src/bin/isa_lines.rs | 76 +- tests/images/metalcase.toml | 15 +- tests/images/panelcase.toml | 2 +- tests/metal/src/lib.rs | 12 +- tests/toyos.rs | 60 +- 88 files changed, 3261 insertions(+), 2552 deletions(-) delete mode 100644 issues/t14-hands-over-an-uninitialised-8042.md delete mode 100644 kernel/loom/tests/i8042_tally.rs delete mode 100644 kernel/src/arch/aarch64/keyboard_controller.rs delete mode 100644 kernel/src/arch/x86_64/i8042/mod.rs delete mode 100644 kernel/src/arch/x86_64/i8042/tally.rs delete mode 100644 kernel/src/arch/x86_64/idt/i8042.rs delete mode 100644 kernel/src/input_merge_test.rs create mode 100644 lib/input/Cargo.toml create mode 100644 lib/input/src/keys.rs create mode 100644 lib/input/src/lib.rs create mode 100644 lib/input/src/merge.rs create mode 100644 lib/input/src/wire.rs create mode 100644 lib/input/tests/input.rs create mode 100644 system/ps2server/Cargo.toml rename {kernel => system/ps2server}/ps2/Cargo.toml (55%) rename {kernel => system/ps2server}/ps2/src/key.rs (90%) rename {kernel => system/ps2server}/ps2/src/lib.rs (76%) rename {kernel => system/ps2server}/ps2/src/mouse.rs (98%) rename {kernel => system/ps2server}/ps2/tests/decode.rs (100%) rename {kernel => system/ps2server}/ps2/tests/fuzz.rs (100%) create mode 100644 system/ps2server/src/bus.rs create mode 100644 system/ps2server/src/fake.rs create mode 100644 system/ps2server/src/hw.rs create mode 100644 system/ps2server/src/main.rs create mode 100644 system/ps2server/src/probe.rs create mode 100644 system/ps2server/src/stream.rs create mode 100644 tests/common/ps2server.rs delete mode 100644 tests/guest/rust-tests/src/bin/isa_claim_refused.rs diff --git a/Cargo.lock b/Cargo.lock index cc037e8b5dc..b0d8a05de4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -554,6 +554,7 @@ dependencies = [ "toyos-abi", "toyos-desktop", "toyos-font", + "toyos-input", "toyos-inspect", "toyos-manifest", "toyos-window", @@ -577,6 +578,7 @@ dependencies = [ "toyos", "toyos-abi", "toyos-font", + "toyos-input", "toyos-logstream", "toyos-window", ] @@ -1833,7 +1835,6 @@ dependencies = [ "toyos-its", "toyos-pci", "toyos-phys", - "toyos-ps2", "toyos-quiesce", "toyos-random", "toyos-rootimage", @@ -2925,6 +2926,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ps2server" +version = "0.1.0" +dependencies = [ + "toyos", + "toyos-abi", + "toyos-input", + "toyos-ps2", +] + [[package]] name = "quick-xml" version = "0.41.0" @@ -4107,6 +4118,7 @@ dependencies = [ "toyos-fat32", "toyos-fat32-check", "toyos-gpt", + "toyos-input", "toyos-keymap", "toyos-logstream", "toyos-manifest", @@ -4136,6 +4148,7 @@ name = "toyos-desktop" version = "0.1.0" dependencies = [ "toyos-abi", + "toyos-input", ] [[package]] @@ -4214,6 +4227,13 @@ dependencies = [ "toyos-device-memory", ] +[[package]] +name = "toyos-input" +version = "0.1.0" +dependencies = [ + "toyos-abi", +] + [[package]] name = "toyos-inspect" version = "0.1.0" @@ -4485,6 +4505,9 @@ dependencies = [ [[package]] name = "toyos-usbhid" version = "0.1.0" +dependencies = [ + "toyos-input", +] [[package]] name = "toyos-userbound" diff --git a/Cargo.toml b/Cargo.toml index daaac56ffae..4618975c497 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ members = [ "kernel/microcode", "kernel/pci", "kernel/phys", - "kernel/ps2", "kernel/sim", "kernel/smmu", "kernel/usbhid", @@ -52,6 +51,7 @@ members = [ "lib/fat32/check", "lib/gpt", "lib/hda", + "lib/input", "lib/inspect", "lib/logkeeper-api", "lib/logstream", @@ -111,6 +111,8 @@ members = [ "system/netstack/udp", "system/netstack/wire", "system/pkg", + "system/ps2server", + "system/ps2server/ps2", "system/shell", "system/soundserver", "system/soundserver/mixer", @@ -207,6 +209,9 @@ toyos-fat32 = { path = "lib/fat32" } # not published until they have both passed on it. toyos-fat32-check = { path = "lib/fat32/check" } toyos-gpt = { path = "lib/gpt" } +# The `input` port's name and the one program the image may give it to, so the +# gate on every manifest reads what its owners and its source read. +toyos-input = { path = "lib/input" } toyos-keymap = { path = "sdk/keymap" } # The form a program's line takes in a boot's log and the port `logkeeper` serves it # on, so the judges reading the log and `logkeeper` writing it read one declaration. diff --git a/build/build.rs b/build/build.rs index 0a0019a16c4..612f17c1fa3 100644 --- a/build/build.rs +++ b/build/build.rs @@ -596,12 +596,14 @@ const SUPERVISOR_SERVED: &[&str] = &[toyos_swap::PORT, "power", toyos_manifest:: /// The ports that one row alone may receive, each with that row: the swap /// port, whose holder replaces any service's binary, the grants port, whose -/// holder writes which folder of the home a package sees, and the prompt -/// port, whose holder draws above every window and has every key while it does. +/// holder writes which folder of the home a package sees, the prompt port, +/// whose holder draws above every window and has every key while it does, and +/// the input port, whose holder types into the focused window. const HELD_ALONE: &[(&str, &str)] = &[ (toyos_swap::PORT, toyos_swap::HOLDER), (toyos_manifest::grants::PORT, toyos_manifest::grants::HOLDER), (toyos_manifest::consent::PROMPT, toyos_manifest::consent::SERVER), + (toyos_input::wire::PORT, toyos_input::wire::HOLDER), ]; /// Who may hold the authorities that change what the machine runs or what a @@ -3125,6 +3127,23 @@ mod tests { } } + /// The input port reaches ps2server alone, and never `[apps]`: a + /// connector types into the focused window. + #[test] + fn only_ps2server_holds_the_input_port() { + let gate = |text: &str| held_by_their_holders_alone(&toml::from_str(text).unwrap()); + assert_eq!(gate("[programs.ps2server]\nreceives = [\"input\"]\n[programs.compositor]\nserves = [\"input\"]\n"), Ok(())); + for (text, said) in [ + ("[programs.paint]\nreceives = [\"input\"]\n", "`paint` receives `input`, which only `ps2server` may hold"), + ("[apps]\nreceives = [\"input\"]\n", "`[apps] receives` names `input`"), + ] { + match gate(text) { + Err(why) if why.contains(said) => {} + other => panic!("{text:?}: {other:?}, not {said:?}"), + } + } + } + /// `[apps] folder` is one of the two accesses, spelled as the manifest /// spells it, and nothing else reaches a manifest. #[test] diff --git a/images/console.toml b/images/console.toml index 5b39e07a2f5..8d54cd8163d 100644 --- a/images/console.toml +++ b/images/console.toml @@ -17,7 +17,7 @@ assets = ["assets"] [boot] -start = ["logkeeper", "diskserver", "fileserver", "console", "acpiserver"] +start = ["logkeeper", "diskserver", "fileserver", "console", "acpiserver", "ps2server"] # `/system/bin/console` owns the screen and provides the surface its shell and the # shell's children inherit. No compositor here, so nothing receives one. @@ -41,8 +41,12 @@ service = true devices = ["acpi"] receives = ["power"] +# `input` makes it the owner of input, as the compositor is where there is one, +# and a row that serves a machine-wide port is a service. [programs.console] +service = true provides = ["surface"] +serves = ["input"] receives = ["log"] starts = ["shell"] devices = ["framebuffer", "keyboard", "mouse"] @@ -51,6 +55,17 @@ devices = ["framebuffer", "keyboard", "mouse"] receives = ["surface"] starts = ["toybox", "shell"] +# The i8042, driven from userland: its ports and its two lines are this +# program's `isa` claim, and the keys and pointer reports it decodes go to the +# owner of input over `input`, which no other row may receive +# (`build/build.rs`). Started again when it ends other than by exit 0, which it +# says only on a machine with nothing to serve. +[programs.ps2server] +restart = true +receives = ["input"] +devices = ["isa:0060,0064:1,12"] + + # `power` because `bin/shutdown` below is this binary, and on the machine this # image exists for — no serial port, a prompt on the panel — there is no other # way to turn it off: it asks the supervisor, which has `logkeeper` make the log whole first. diff --git a/images/diag.toml b/images/diag.toml index b19369c177d..05f9a36a350 100644 --- a/images/diag.toml +++ b/images/diag.toml @@ -5,8 +5,7 @@ # phase boundary and returns immediately once anything claims # DEVICE_FRAMEBUFFER. On the image that was flashed to the T14 the compositor # claimed it tens of milliseconds after the last checkpoint, so every line the -# kernel logged — including the one `i8042:` line that says why the integrated -# keyboard is dead — was on screen for about a tenth of a second and then gone. +# kernel logged was on screen for about a tenth of a second and then gone. # # `/system/bin/supervisor` is in every image and holds the one `SysCap`, so the # guarantee is that this config declares no `devices`, which diff --git a/images/system.toml b/images/system.toml index bb677b274be..d215aef0dc1 100644 --- a/images/system.toml +++ b/images/system.toml @@ -19,7 +19,7 @@ assets = ["assets"] # while the compositor had no way to ask for a program's *declared* authority — # the launcher gives it one, and starting a service from `[boot] start` is not # the thing that needed fixing. -start = ["logkeeper", "diskserver", "fileserver", "compositor", "soundserver", "netstack", "filepicker", "acpiserver"] +start = ["logkeeper", "diskserver", "fileserver", "compositor", "soundserver", "netstack", "filepicker", "acpiserver", "ps2server"] # What every program launched out of `/apps` holds, whatever its own directory # says. A package is unpacked by `/system/bin/pkg` into a writable directory, so @@ -69,15 +69,26 @@ receives = ["power"] # makes the local desktop a login session, so `swap` and `update` run from its # shell. `prompt` is the layer above every window, which `filepicker` alone # reaches: serving it makes the sessions its launches open the screen's, whose -# packages may ask the person at it for a folder. +# packages may ask the person at it for a folder. `input` is where a source of +# keys and pointer reports reaches the owner of input, which it is. [programs.compositor] service = true -serves = ["compositor", "prompt"] +serves = ["compositor", "prompt", "input"] receives = ["soundserver", "filepicker"] devices = ["framebuffer", "keyboard", "mouse"] starts = ["terminal", "files", "/apps"] login = true +# The i8042, driven from userland: its ports and its two lines are this +# program's `isa` claim, and the keys and pointer reports it decodes go to the +# owner of input over `input`, which no other row may receive +# (`build/build.rs`). Started again when it ends other than by exit 0, which it +# says only on a machine with nothing to serve. +[programs.ps2server] +restart = true +receives = ["input"] +devices = ["isa:0060,0064:1,12"] + [programs.soundserver] service = true serves = ["soundserver"] diff --git a/issues/t14-hands-over-an-uninitialised-8042.md b/issues/t14-hands-over-an-uninitialised-8042.md deleted file mode 100644 index 09ee74fe958..00000000000 --- a/issues/t14-hands-over-an-uninitialised-8042.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-08-07 ---- - -# The T14's firmware hands over an *uninitialised* 8042 about one boot in seven, and the fallback has nothing to stand on there - -Found across seven consecutive boots of one image, 22:26–22:33 on 2026-08-07. -Six read `cfg=0x77->0x64`. The boot logged at `222741` reads `cfg=0x30->0x60`, and the -driver disabled the keyboard by name: - -``` -i8042: kbd DISABLED - the set query answered 0xee and firmware's cfg 0x30 has - translate off, so nothing says what the wire carries -``` - -**Subtract our own two commands and the number says more than the message -does.** `before` is read *after* `CMD_DISABLE_PORT1` and `CMD_DISABLE_AUX`, so -bits 4 and 5 of it are always ours. Firmware's byte was therefore `0x47` on the -six — kbd IRQ, aux IRQ, system flag, translate — and **`0x00` on the seventh**. -Bit 2 is the system flag POST sets to say it has initialised the controller. So -that boot was handed an 8042 in its power-on default: firmware had not touched -it at all. - -The refusal is correct as written and this is not a request to weaken it. What -the entry records is that **the evidence the fallback rests on is not always -there**, on this machine, at a rate of roughly one boot in seven — and the cost -when it is absent is the whole of the machine's integrated input, because the -refusal returns before the aux port too. The owner sees a desktop with a dead -keyboard and a dead TrackPoint, and the one line saying why is in a log he can -only read after rebooting. In the 2026-08-07 set that boot is one of the five -"freezes", and it is not one. - -Two smaller things fall out and neither is fixed: - -- **The message calls `0x30` "firmware's own cfg" and two of its bits are - ours.** The inference is untouched — bit 6 is firmware's — but the label - overclaims, and the value a reader is asked to reason about is not the value - the sentence names. -- **What a correct answer would even be is a policy question, not a bug.** The - controller is put into translating mode by `wanted` regardless of what - firmware left, so the open question is only what the *device* emits, and on - this EC neither `0xF0 0x00` nor `0xF2` may be asked (see `t14-keyboard-will-not-report-its-scancode-set`). - Guessing is the outcome the read-back exists to prevent; refusing costs the - keyboard. It is the owner's call which way this machine should fail. diff --git a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md index f5309e30184..78cb06e9d10 100644 --- a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md +++ b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md @@ -17,18 +17,17 @@ kernel's part of a restart, and named module tests hold each program's ## What is left, in order -1. **ps2server**: stage 7 step 2, with stage 6 step 3. -2. **Step 9's file half** (stage 4), once libc is a file server's client +1. **Step 9's file half** (stage 4), once libc is a file server's client (`issues/c-programs-name-no-file-a-file-server-holds.md`). -3. **Step 8's exit** (stage 4). Its empty view needs step 9's file half: +2. **Step 8's exit** (stage 4). Its empty view needs step 9's file half: until then the kernel serves `/system` and `/tmp` to every process. -4. **P1 of the Wi-Fi track** (`issues/there-is-no-wifi.md`): a BAR stays +3. **P1 of the Wi-Fi track** (`issues/there-is-no-wifi.md`): a BAR stays where firmware put it, handed over in 4 KiB pages with its MSI-X table's page, under the owner's Q2 ruling. The orchestrator ruled on #900 that this route serves usbd's claim (step 10). -5. **usbd's whole xHCI**: step 10, with stage 6 step 4, step 4's tail and +4. **usbd's whole xHCI**: step 10, with stage 6 step 4, step 4's tail and step 9's block half, which serve the stick until usbd does. -6. **Stage 6 step 5, then stage 5's windows.** +5. **Stage 6 step 5, then stage 5's windows.** Beside that order, each waiting on something outside it: - stage 1's NVMe boot-time measurement, on a ROOT on the T14's NVMe, which @@ -288,7 +287,7 @@ Beside that order, each waiting on something outside it: 3. **The i8042's thread is ps2server's** (stage 7 step 2): `irq_ring`'s `I8042`, `keyboard_controller::service` and the idle loop's `verdict_due` go with the kernel's driver. **Exit**: that step's. - **Not started.** + **Done** (#899). 4. **xHCI's thread is usbd's** (stage 4 step 10): `Xhci`, `poll_if_pending` and `port_work_pending` go with the kernel's driver, and `irq_ring` and `sync::OwedLock`, whose one user is `XHCI`, with @@ -302,26 +301,25 @@ Beside that order, each waiting on something outside it: are met. **Not started.** 7. **The i8042 leaves the kernel.** Owner ruling, 2026-09-28: drivers are userland, and a dead kernel takes no input, with no emergency way. - **Partly done**: step 1. + **Done**. 1. **The panic console takes no input, and an `isa` claim grants a process exact ports and its ISA lines.** **Done** (#592). - 2. **ps2server**, the server over that claim, feeding the kernel's keyboard - and mouse streams so the merge with USB HID stays where it is; the - kernel's driver, its vector, its actuators and the - `keyboard_controller` seam deleted. A keyboard claim is refused while no - source exists, which the supervisor's order of endowment then decides. - **Exit**: no i8042 code in the kernel (`arch/x86_64/i8042/`, - `arch/x86_64/idt/i8042.rs` and both architectures' - `keyboard_controller` gone from `kernel/src`, and `toyos-ps2`, whose - one kernel user is `kernel/src/arch/x86_64/i8042/mod.rs`, gone from - `kernel/Cargo.toml`); a kernel test this step owes, that a keyboard - claim whose holder dies is handed to the next claimant and the - keyboard stream delivers its transitions and none of the dead - holder's, red on a kernel that keeps the dead holder as the source; + 2. **ps2server**, the server over that claim, delivering over IPC to the + owner of input, which merges every source; the kernel's merge deleted, + its keyboard and mouse streams carrying its own sources' records + unmerged until usbd takes the last of them (step 10), which deletes + them; the kernel's driver, its vector, its actuators and the + `keyboard_controller` seam deleted. **Exit**: no i8042 code in the + kernel (`arch/x86_64/i8042/`, `arch/x86_64/idt/i8042.rs` and both + architectures' `keyboard_controller` gone from `kernel/src`, and + `toyos-ps2` gone from `kernel/Cargo.toml`); a kernel test that an + `isa` claim whose holder dies is handed to the next claimant with none + of the dead holder's interrupts, red on a kernel whose release keeps + the dead holder's record: `isa_lines_reach_their_holder`'s `dies` role; and ps2server's restart mid-packet held by `toyos-ps2`'s `a_lost_scancode_byte_never_wedges_the_prefix_state` and `a_stream_truncated_at_any_offset_resyncs_within_two_packets`. - **Not started.** + **Done** (#899). ## Standing diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 03c0089de92..92331b48eaf 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -495,7 +495,6 @@ toyos-phys = { path = "phys" } toyos-smmu = { path = "smmu" } toyos-tco = { path = "../lib/tco" } toyos-tsc = { path = "../lib/tsc" } -toyos-ps2 = { path = "ps2" } toyos-rootimage = { path = "../lib/rootimage" } toyos-quiesce = { path = "../lib/quiesce" } toyos-random = { path = "../lib/random" } diff --git a/kernel/loom/src/lib.rs b/kernel/loom/src/lib.rs index 003d71dffc5..8d67af144ac 100644 --- a/kernel/loom/src/lib.rs +++ b/kernel/loom/src/lib.rs @@ -1,15 +1,14 @@ //! Loom harness for the kernel's memory-ordering primitives. //! -//! `kernel/src/sync.rs`, `kernel/src/shootdown.rs`, -//! `kernel/src/sched/reap_gate.rs` and `kernel/src/arch/x86_64/i8042/tally.rs` are -//! compiled into this crate with `feature = "loom"` on, so their atomics and +//! `kernel/src/sync.rs`, `kernel/src/shootdown.rs` and +//! `kernel/src/sched/reap_gate.rs` are compiled into this crate with `feature = "loom"` on, so their atomics and //! cells resolve to loom's instrumented ones and the models drive the real //! primitives rather than transliterations of them — a transliteration is //! exactly the divergence risk a model checker is meant to remove. What the //! kernel files name through `crate::` is supplied below: the lock takes a //! preempt count and a log macro from its environment, and neither is what the -//! models are about; `shootdown.rs`, `reap_gate.rs` and `tally.rs` name nothing -//! at all, which is why none of the three has a shim here. +//! models are about; `shootdown.rs` and `reap_gate.rs` name nothing at all, +//! which is why neither has a shim here. //! //! Scope for the lock, stated because it is narrower than the file: the models //! drive `try_lock` and `LockGuard::drop`. `lock()`'s spin cannot be modelled — @@ -379,13 +378,6 @@ pub mod sleeplock; #[path = "../../src/log/handoff.rs"] pub mod log_handoff; -/// The i8042's interrupt tally. `tests/i8042_tally.rs` is the only model whose -/// subject is a *driver*, and it is here for the reason the others are: the -/// property is "no reader ever sees this pair disagree", which is a claim about -/// instants that no guest test can express and that x86's TSO hides. -#[path = "../../src/arch/x86_64/i8042/tally.rs"] -pub mod i8042_tally; - /// The panic snapshot's owner and access state, driven together by /// `tests/panic_capture.rs`. #[path = "../../src/drivers/panic_console/latch.rs"] diff --git a/kernel/loom/tests/i8042_tally.rs b/kernel/loom/tests/i8042_tally.rs deleted file mode 100644 index 4e307be243c..00000000000 --- a/kernel/loom/tests/i8042_tally.rs +++ /dev/null @@ -1,214 +0,0 @@ -//! Loom: the i8042's interrupt tally. -//! -//! The driver prints `N interrupts and M bytes, nothing decoded` — a verdict -//! about *bytes* — and it decides whether to print it from how many interrupts -//! carried one. That was two counters the ISR wrote at either end of its -//! port-drain burst, subtracted by a reader on another CPU, and a reader landing -//! between the two writes blamed an interrupt that had carried nothing. -//! -//! The same handler counting on the way *in*, ahead of any byte reaching the -//! ring, needs no subtraction at all to blame an interrupt that carried -//! nothing. One word closes both. -//! -//! **A rate is why this is a model and not a test.** Either window is a handful -//! of instructions on one CPU; no guest boot can be made to land in one on -//! demand, and a green suite says nothing about whether it can still be landed -//! in. What the property actually is — *at no instant may a reader see an empty -//! interrupt counted as one that carried* — is a claim about every -//! interleaving, which is what loom enumerates. -//! -//! Three directions, and the third is the one that makes the first two mean -//! anything: -//! -//! * **The property.** An interrupt that delivered nothing is never counted as -//! one that did, from any interleaving. -//! * **The cost it must not have.** An interrupt that *did* deliver is counted, -//! and a reader that sees the count can see the bytes. -//! * **The teeth.** The same reader, against the shape this replaced, must -//! actually observe the bad state. A model that has never seen a defect is a -//! model nobody knows is exploring anything. -//! -//! **Both of the first two red when the two counters are put back**, measured by -//! putting them back: `Counts { carried: 1, empty: 0 }` for an interrupt that -//! carried nothing, and a count of an arrived byte with the byte not yet -//! published. That run is the evidence this file works and the evidence the fix -//! does; the guest suite can supply neither, because a rate is not falsified by -//! a green boot. - -use std::sync::atomic::{AtomicBool, Ordering as StdOrdering}; - -use kernel_loom::i8042_tally::{Carried, Counts, Tally}; -use loom::sync::atomic::{AtomicU32, Ordering}; -use loom::sync::Arc; - -/// An interrupt the ISR found nothing behind is never counted as one that -/// carried a byte — at the settled end, and at every instant on the way there. -/// -/// The reader is `service`'s `report_health` on any other CPU: it runs at the -/// top of every scheduler pass, so it is genuinely concurrent with the ISR and -/// is not synchronized against it by anything. -#[test] -fn an_empty_interrupt_is_never_counted_as_one_that_carried() { - static READ_AFTER_RECORD: AtomicBool = AtomicBool::new(false); - - loom::model(|| { - let tally = Arc::new(Tally::new()); - - let isr = { - let tally = tally.clone(); - loom::thread::spawn(move || tally.record(Carried::Nothing)) - }; - - // The verdict's own question: "did anything arrive to decode?" - let mid = tally.read(); - assert_eq!( - mid.carried, 0, - "a reader saw {mid:?} while the only interrupt on the machine carried nothing — \ - `nothing decoded` would have been printed about a byte that never arrived", - ); - // And the total it prints alongside can never exceed what happened. - assert!(mid.irqs() <= 1, "a reader saw {mid:?}, which is more interrupts than were taken"); - if mid.irqs() == 1 { - READ_AFTER_RECORD.store(true, StdOrdering::Relaxed); - } - - isr.join().unwrap(); - assert_eq!( - tally.read(), - Counts { carried: 0, empty: 1 }, - "the settled pair does not account for the one interrupt that was taken", - ); - }); - - assert!( - READ_AFTER_RECORD.load(StdOrdering::Relaxed), - "no reader ran after the ISR recorded the interrupt, so no counted read was checked", - ); -} - -/// The property the fix must not cost: an interrupt that delivered bytes *is* -/// counted, and a reader that sees the count can see what it delivered. -/// -/// The store stands for the byte ring's `HEAD`, which the ISR publishes before -/// it records the interrupt. Every reader of `carried` is about to say something -/// about those bytes — `report_health` checks `has_bytes()` the instant after — -/// so a count visible ahead of its own evidence is a report on a ring that looks -/// empty. **This is the direction that reds on the old shape**: with the two -/// counters back in `tally.rs` it failed here too, `published` still 0 under a -/// count that already said a byte had arrived. -#[test] -fn a_counted_interrupt_carries_its_bytes_with_it() { - static COUNTED: AtomicBool = AtomicBool::new(false); - - loom::model(|| { - let tally = Arc::new(Tally::new()); - let published = Arc::new(AtomicU32::new(0)); - - let isr = { - let (tally, published) = (tally.clone(), published.clone()); - loom::thread::spawn(move || { - // Everything the interrupt did, before it says it happened. - published.store(1, Ordering::Relaxed); - tally.record(Carried::Bytes); - }) - }; - - let seen = tally.read(); - if seen.carried > 0 { - assert_eq!( - published.load(Ordering::Relaxed), - 1, - "a reader counted an interrupt as having delivered a byte and could not see the \ - byte: the report would say `0 bytes` about one that had arrived", - ); - COUNTED.store(true, StdOrdering::Relaxed); - } - - isr.join().unwrap(); - assert_eq!( - tally.read(), - Counts { carried: 1, empty: 0 }, - "an interrupt that delivered bytes was not counted as one", - ); - }); - - assert!( - COUNTED.load(StdOrdering::Relaxed), - "no reader saw the interrupt counted, so the bytes behind a count were never checked", - ); -} - -/// The shape this replaced, kept as the model's own negative control. -/// -/// **Deliberately a transliteration**, and the one place in this crate where -/// that is right: it is not the kernel's code any more, and what it has to -/// reproduce is the *arithmetic* — a total written on the way in, an empty count -/// written on the way out, and a reader subtracting one from the other. The -/// point of the model below is that the arithmetic is unsound however faithfully -/// it is copied, which is why narrowing the drain between the two writes was -/// never a fix. -struct TornTally { - irqs: AtomicU32, - empty: AtomicU32, -} - -impl TornTally { - fn new() -> Self { - Self { irqs: AtomicU32::new(0), empty: AtomicU32::new(0) } - } - - /// `handler()` as it was: `IRQS` on entry, the port-drain burst, then - /// `EMPTY_IRQS` because the burst read nothing. The burst itself is not - /// modelled — it is what made the window wide, and the window's *width* is - /// what this file exists to say is beside the point. - fn record_empty(&self) { - self.irqs.fetch_add(1, Ordering::Relaxed); - self.empty.fetch_add(1, Ordering::Relaxed); - } - - /// `report_health`'s own line, verbatim. - fn carried(&self) -> u32 { - self.irqs.load(Ordering::Relaxed).saturating_sub(self.empty.load(Ordering::Relaxed)) - } -} - -/// The teeth: the reader above, against the two counters, really does see the -/// state that printed the false verdict. -/// -/// Asserted by collecting rather than by failing, so this stays a passing test -/// that proves a failure is reachable — the flag is an ordinary `std` atomic and -/// therefore outlives loom's executions, and the assertion after `loom::model` -/// is the whole verdict. If a future loom, or a future edit to the model's -/// shape, stops scheduling a reader inside the ISR's window, **this reds** and -/// says so: at that point the two models above are passing because nothing is -/// being explored, which is the one failure a gate must not have. -#[test] -fn the_split_counters_this_replaced_are_read_torn() { - static TORN: AtomicBool = AtomicBool::new(false); - - loom::model(|| { - let torn = Arc::new(TornTally::new()); - - let isr = { - let torn = torn.clone(); - loom::thread::spawn(move || torn.record_empty()) - }; - - if torn.carried() > 0 { - TORN.store(true, StdOrdering::Relaxed); - } - - isr.join().unwrap(); - // Settled, the subtraction was always right. That is exactly why the - // defect survived review: every interleaving ends in the correct answer - // and only the instants in between are wrong. - assert_eq!(torn.carried(), 0, "the split counters disagree once the ISR has finished"); - }); - - assert!( - TORN.load(StdOrdering::Relaxed), - "no interleaving read the two counters torn, so this model cannot tell the shape that \ - printed the false verdict from the one that replaced it — the two models above are \ - passing on an exploration that is not happening", - ); -} diff --git a/kernel/pci/Cargo.toml b/kernel/pci/Cargo.toml index fefa1e05760..1b735bbfe8c 100644 --- a/kernel/pci/Cargo.toml +++ b/kernel/pci/Cargo.toml @@ -1,5 +1,5 @@ -# A member of the host workspace (root `Cargo.toml`), like toyos-gpt, toyos-hda -# and toyos-ps2: the kernel depends on it by path and its tests run on the host. +# A member of the host workspace (root `Cargo.toml`), like toyos-gpt and +# toyos-hda: the kernel depends on it by path and its tests run on the host. # What lives here is the arithmetic between a capability structure a *device* # published and the address the kernel is about to write an interrupt vector # through — the one place where believing a device costs the machine a write to diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index da65626226b..b4ba1206bbd 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -52,14 +52,6 @@ actuators! { /// AMD `SYSRET` SS-attributes workaround's only guest-observable proof. sysret_ss_probe = "sysret-ss-probe"; - /// Leave the i8042 unprobed, so this kernel drives no controller and an - /// `isa` claim on it is granted. Judged by `isa_ports_are_the_binders_alone` - /// and `isa_lines_reach_their_holder`. - i8042_withheld = "i8042-withheld"; - - /// Script the input core directly at end of boot. - test_input_merge = "test-input-merge"; - /// Run the xHCI extended-capability walk over eight malformed lists at init. xhci_xecp_selftest = "xhci-xecp-selftest"; diff --git a/kernel/src/arch/aarch64/keyboard_controller.rs b/kernel/src/arch/aarch64/keyboard_controller.rs deleted file mode 100644 index cebd8f04265..00000000000 --- a/kernel/src/arch/aarch64/keyboard_controller.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! The platform's own keyboard controller. An Arm machine has none: its -//! keyboards are USB. - -/// Nothing to decide about a controller that is not there. -pub fn verdict_due() -> bool { - false -} - -/// Nothing to service. -pub fn service() {} - -/// Nothing to report. -pub fn report_line() {} diff --git a/kernel/src/arch/aarch64/mod.rs b/kernel/src/arch/aarch64/mod.rs index 6d9c87c2cce..fe6c745360e 100644 --- a/kernel/src/arch/aarch64/mod.rs +++ b/kernel/src/arch/aarch64/mod.rs @@ -24,7 +24,6 @@ pub mod entry; pub mod fpu; pub mod hw; pub mod irqchip; -pub mod keyboard_controller; pub mod paging; pub mod percpu; pub mod pio; diff --git a/kernel/src/arch/x86_64/boot.rs b/kernel/src/arch/x86_64/boot.rs index 6396ec8cee1..59f682fe1e5 100644 --- a/kernel/src/arch/x86_64/boot.rs +++ b/kernel/src/arch/x86_64/boot.rs @@ -137,7 +137,6 @@ pub fn timer() { /// The platform's own devices that are not PCI functions. pub fn platform_devices(platform: &Platform, rsdp_addr: u64) { - super::i8042::init(rsdp_addr); super::pio::fill_i8042_row(); super::acpi_mode::init(rsdp_addr, platform.facs); } diff --git a/kernel/src/arch/x86_64/i8042/mod.rs b/kernel/src/arch/x86_64/i8042/mod.rs deleted file mode 100644 index 1e2b247e1f2..00000000000 --- a/kernel/src/arch/x86_64/i8042/mod.rs +++ /dev/null @@ -1,1284 +0,0 @@ -//! The i8042 PS/2 controller: keyboard and TrackPoint aux port. -//! -//! The ISR is the sole reader of port 0x60 and must never take a `Lock`, -//! allocate, call `log!`, wake a waiter, or loop unboundedly — see -//! [`handler`]. Delivery is pinned to one CPU (`IRQ_CPU`); the drain's own -//! polled port I/O must run there too, with interrupts off. Init treats the -//! controller as untrusted hardware: every wait is bounded and a failure -//! costs the keyboard, never the boot. - -use core::sync::atomic::{AtomicBool, AtomicU16, AtomicU32, AtomicU64, AtomicU8, Ordering}; - -use toyos_ps2::{KeyDecoder, KeyOutcome, MouseDecoder, MouseOutcome}; - -use crate::arch::cpu::{inb, outb}; -use crate::arch::pio::{self, Port}; -use crate::arch::idt::I8042_VECTOR; -use crate::irq_ring::IrqSource; -use crate::log; -use crate::sync::Lock; -use crate::time::{Budget, Cadence, Duration}; -use super::ioapic::{self, Gsi}; - -mod tally; - -use tally::{Carried, Tally}; - -/// The controller's data port, and its status port, which is its command -/// port when written; declared by [`init`] before its first access. -static DATA: pio::Slot = pio::Slot::empty(); -static STATUS: pio::Slot = pio::Slot::empty(); - -fn port(run: &pio::Slot) -> Port { - run.get().expect("i8042: a port reached before init declared it").port(0) -} - -const OBF: u8 = 1 << 0; -const IBF: u8 = 1 << 1; -const AUXB: u8 = 1 << 5; - -/// `IAPC_BOOT_ARCH` bit 1 (ACPI 6.5 §5.10): "the motherboard has a port -/// 60/64 keyboard controller". -const FADT_8042: u16 = 1 << 1; - -/// A floating bus reads `0xff`; every wait below gives up on this value -/// rather than only a deadline, so a machine with no controller doesn't -/// spend the whole init budget waiting on hardware that isn't there. -const FLOATING_BUS: u8 = 0xFF; - -const CMD_READ_CONFIG: u8 = 0x20; -const CMD_WRITE_CONFIG: u8 = 0x60; -const CMD_DISABLE_AUX: u8 = 0xA7; -const CMD_ENABLE_AUX: u8 = 0xA8; -const CMD_TEST_AUX: u8 = 0xA9; -const CMD_SELF_TEST: u8 = 0xAA; -const CMD_TEST_PORT1: u8 = 0xAB; -const CMD_DISABLE_PORT1: u8 = 0xAD; -const CMD_ENABLE_PORT1: u8 = 0xAE; -const CMD_WRITE_AUX: u8 = 0xD4; - -const CFG_PORT1_IRQ: u8 = 1 << 0; -const CFG_PORT2_IRQ: u8 = 1 << 1; -const CFG_PORT1_CLOCK_OFF: u8 = 1 << 4; -const CFG_PORT2_CLOCK_OFF: u8 = 1 << 5; -const CFG_TRANSLATE: u8 = 1 << 6; - -const ISA_IRQ_KEYBOARD: u8 = 1; -const ISA_IRQ_AUX: u8 = 12; - -/// Larger than any legitimate burst (3-byte mouse packet + 4-byte extended -/// key); past this the drain masks the line rather than hold the CPU. -const ISR_BURST: usize = 16; - -static ACTIVE: AtomicBool = AtomicBool::new(false); -static QUARANTINE: AtomicBool = AtomicBool::new(false); -static KBD_EVENTS: AtomicU32 = AtomicU32::new(0); -static AUX_EVENTS: AtomicU32 = AtomicU32::new(0); -static LOST_EDGES: AtomicU32 = AtomicU32::new(0); -static DROPPED: AtomicU32 = AtomicU32::new(0); -static DROPPED_TOTAL: AtomicU32 = AtomicU32::new(0); -/// Pointer bytes the framer discarded resyncing. Zero in a healthy stream. -static DISCARDS: AtomicU32 = AtomicU32::new(0); -/// Set-1 overrun codes: a keyboard reporting lost bytes, not one we cannot decode. -static OVERRUNS: AtomicU32 = AtomicU32::new(0); -static KEYBOARD_GSI: AtomicU32 = AtomicU32::new(u32::MAX); -static AUX_GSI: AtomicU32 = AtomicU32::new(u32::MAX); - -/// Interrupts split by whether the ISR found a byte behind them. One atomic -/// value, not two counters (`tally.rs`): read mid-burst, two counters would -/// answer "did anything arrive" wrongly. -static TALLY: Tally = Tally::new(); - -/// Bytes taken off the ring, counted in [`pop`] before the byte leaves it — -/// so a byte is always in the ring or in this count, never in neither. -static RX_BYTES: AtomicU32 = AtomicU32::new(0); - -/// When the pin first asserted. Set only in the ISR — never in -/// `handler_poll`, whose bytes came from a poll, not an assertion. -static FIRST_IRQ_NS: AtomicU64 = AtomicU64::new(0); - -/// `init` consumed a byte with the IRQ bits live, so its edge can deliver empty; the first interrupt settles it. -static ARM_EDGE_OWED: AtomicBool = AtomicBool::new(false); -/// That first interrupt was the arming edge: empty, its byte already init's. -static ARM_EDGE_CONSUMED: AtomicBool = AtomicBool::new(false); - -/// When the pin last asserted. -static LAST_IRQ_NS: AtomicU64 = AtomicU64::new(0); - -/// The CPU the vector is pinned to: the sole reader of port 0x60, and the -/// only CPU an `irq_ring` record for this source can exist on. -static IRQ_CPU: AtomicU32 = AtomicU32::new(u32::MAX); - -fn is_irq_cpu() -> bool { - IRQ_CPU.load(Ordering::Relaxed) == crate::arch::percpu::cpu_id() -} - -// The first interrupt and the quiet verdict are each reported exactly once, -// from the pass that discovers them — never a wall-clock deadline, since -// `service` only runs inside a scheduler pass and an idle boot may never -// enter one again to notice it expired. - -/// `init` never armed the pin, so there is nothing to say. -const HEALTH_OFF: u8 = 0; -/// Armed and watching. -const HEALTH_ARMED: u8 = 1; -/// A CPU ran out of work; the quiet verdict is owed and the next pass emits it. -const HEALTH_QUIET_DUE: u8 = 2; -/// The quiet verdict is out; still watching, since a later interrupt answers it. -const HEALTH_QUIET_SAID: u8 = 3; -/// The boot verdict is out; the counters speak for themselves from here. -const HEALTH_DONE: u8 = 4; -/// Bytes arrived and decoded to nothing, said naming them; still watching, -/// since the byte that completes a sequence may be one interrupt away. -const HEALTH_MUTE_SAID: u8 = 5; -/// The pin asserted with no byte behind it, distinct from `HEALTH_MUTE_SAID` -/// so the first byte that does decode to nothing is still reported. -const HEALTH_EMPTY_SAID: u8 = 6; -/// The mute verdict beat the sequence and named nothing; revised once, to -/// [`HEALTH_MUTE_SAID`], when a byte is first blamed. -const HEALTH_MUTE_BLIND: u8 = 7; - -static HEALTH: AtomicU8 = AtomicU8::new(HEALTH_OFF); -static ARMED_NS: AtomicU64 = AtomicU64::new(0); - -// Repeats, but only when the pin has asserted since the last line — so past -// the first repeat, silence means no interrupt, not a driver that stopped. -fn health_period_ns() -> u64 { - /// A log-rate limit; an idle machine pays nothing. - const HEALTH: Cadence = Cadence::every( - Duration::from_secs(10), - "the PMM dump's own cadence, and one line per 10s of typing", - ); - HEALTH.nanos() -} -static NEXT_REPORT_NS: AtomicU64 = AtomicU64::new(u64::MAX); -static REPORTED_IRQS: AtomicU32 = AtomicU32::new(0); - -/// Bytes the drain did not turn into an event, oldest first — a byte a -/// decoder is still holding is not one of these. -const UNEXPLAINED_LEN: usize = 8; -static UNEXPLAINED: [AtomicU16; UNEXPLAINED_LEN] = [const { AtomicU16::new(0) }; UNEXPLAINED_LEN]; -static UNEXPLAINED_N: AtomicU32 = AtomicU32::new(0); -const UNEXPLAINED_AUX: u16 = 1 << 8; - -fn record_unexplained(byte: u8, aux: bool) { - let n = UNEXPLAINED_N.fetch_add(1, Ordering::Relaxed) as usize; - if let Some(slot) = UNEXPLAINED.get(n) { - slot.store(u16::from(byte) | if aux { UNEXPLAINED_AUX } else { 0 }, Ordering::Relaxed); - } -} - -/// Renders ` no event from [0xe0, aux 0x08],` — empty when every byte decoded. -struct Unexplained; - -impl core::fmt::Display for Unexplained { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let seen = UNEXPLAINED_N.load(Ordering::Relaxed) as usize; - if seen == 0 { - return Ok(()); - } - write!(f, " no event from [")?; - for (i, slot) in UNEXPLAINED.iter().take(seen).enumerate() { - let value = slot.load(Ordering::Relaxed); - if i > 0 { - write!(f, ", ")?; - } - if value & UNEXPLAINED_AUX != 0 { - write!(f, "aux ")?; - } - write!(f, "{:#04x}", value as u8)?; - } - if seen > UNEXPLAINED_LEN { - write!(f, ", +{}", seen - UNEXPLAINED_LEN)?; - } - write!(f, "],") - } -} - -fn claim_health(from: u8, to: u8) -> bool { - HEALTH - .compare_exchange(from, to, Ordering::Relaxed, Ordering::Relaxed) - .is_ok() -} - -/// Called from the idle loop, interrupts off, before halting: pure atomics, -/// no lock, no port I/O. `true` keeps the CPU awake for exactly one more -/// pass; an inactive driver answers `false`, so nothing spins. -pub fn verdict_due() -> bool { - if !ACTIVE.load(Ordering::Relaxed) { - return false; - } - match HEALTH.load(Ordering::Relaxed) { - HEALTH_ARMED => { - claim_health(HEALTH_ARMED, HEALTH_QUIET_DUE); - true - } - HEALTH_QUIET_DUE => true, - _ => false, - } -} - -fn millis_since_boot() -> u64 { - crate::clock::nanos_since_boot() / 1_000_000 -} - -fn first_irq_ms() -> u64 { - FIRST_IRQ_NS.load(Ordering::Relaxed) / 1_000_000 -} - -/// Say once whether the armed pin has ever asserted; "nothing decoded" is -/// claimed only once every arrived byte is accounted for, via the -/// `has_bytes` guard. -fn report_health(state: u8) { - let counts = TALLY.read(); - let irqs = counts.irqs(); - if counts.carried > 0 || RX_BYTES.load(Ordering::Relaxed) > 0 { - if has_bytes() { - return; - } - let keys = KBD_EVENTS.load(Ordering::Relaxed); - let motion = AUX_EVENTS.load(Ordering::Relaxed); - // A further line fires only when the picture changes — blind to named - // when the run ends and blames its bytes, mute to decoded when a key - // arrives — so a half-arrived keystroke never freezes here as DONE. - let next = if keys + motion > 0 { - HEALTH_DONE - } else if UNEXPLAINED_N.load(Ordering::Relaxed) == 0 { - HEALTH_MUTE_BLIND - } else { - HEALTH_MUTE_SAID - }; - if next == state || !claim_health(state, next) { - return; - } - if next != HEALTH_DONE { - log!( - "i8042: {} interrupts and {} bytes, nothing decoded —{} first seen at {}ms", - irqs, - RX_BYTES.load(Ordering::Relaxed), - Unexplained, - first_irq_ms() - ); - } else { - log!( - "i8042: the pin asserts — {} interrupts, {} bytes, {} keys, {} motion,{} first seen at {}ms", - irqs, - RX_BYTES.load(Ordering::Relaxed), - keys, - motion, - Unexplained, - first_irq_ms() - ); - } - to_screen(); - arm_repeat(); - return; - } - // The third case: the pin asserts but nothing has come over it — distinct - // from "nothing decoded" (bytes exist) and "never asserted" below. - if irqs > 0 { - // Except init's own echo — the arming edge: the quiet verdict stands saying so. - if irqs == 1 && counts.empty == 1 && ARM_EDGE_CONSUMED.load(Ordering::Relaxed) { - if state == HEALTH_QUIET_DUE && claim_health(HEALTH_QUIET_DUE, HEALTH_QUIET_SAID) { - log!( - "i8042: armed at {}ms, idle at {}ms, 1 interrupt, the arming edge (its byte \ - was init's own read-back) — the pin has never asserted for input (kbd GSI \ - {}, aux GSI {})", - ARMED_NS.load(Ordering::Relaxed) / 1_000_000, - millis_since_boot(), - KEYBOARD_GSI.load(Ordering::Relaxed) as i64, - AUX_GSI.load(Ordering::Relaxed) as i64 - ); - to_screen(); - } - return; - } - if state != HEALTH_EMPTY_SAID && claim_health(state, HEALTH_EMPTY_SAID) { - log!( - "i8042: {} interrupts and no byte behind any of them — the output buffer was empty when the ISR read it, first seen at {}ms", - irqs, - first_irq_ms() - ); - to_screen(); - arm_repeat(); - } - return; - } - if state == HEALTH_QUIET_DUE && claim_health(HEALTH_QUIET_DUE, HEALTH_QUIET_SAID) { - log!( - "i8042: armed at {}ms, idle at {}ms, 0 interrupts — the pin has never asserted (kbd GSI {}, aux GSI {})", - ARMED_NS.load(Ordering::Relaxed) / 1_000_000, - millis_since_boot(), - KEYBOARD_GSI.load(Ordering::Relaxed) as i64, - AUX_GSI.load(Ordering::Relaxed) as i64 - ); - to_screen(); - } -} - -fn arm_repeat() { - NEXT_REPORT_NS.store(crate::clock::nanos_since_boot() + health_period_ns(), Ordering::Relaxed); -} - -/// Say the counters again, at most once per [`health_period_ns`] and only -/// when the pin has asserted since the last line. No `to_screen`: the ring -/// is the primary channel and this line already appears there. -fn report_counters() { - let counts = TALLY.read(); - let irqs = counts.irqs(); - if irqs == REPORTED_IRQS.load(Ordering::Relaxed) { - return; - } - let now = crate::clock::nanos_since_boot(); - let next = NEXT_REPORT_NS.load(Ordering::Relaxed); - if now < next - || NEXT_REPORT_NS - .compare_exchange(next, now + health_period_ns(), Ordering::Relaxed, Ordering::Relaxed) - .is_err() - { - return; - } - REPORTED_IRQS.store(irqs, Ordering::Relaxed); - // `empty` is read from the same snapshot as `irqs`, so the two counts - // cannot disagree about how many interrupts there were. - log!( - "i8042: {} interrupts, {} bytes, {} keys, {} motion, {} undecoded, {} discarded, {} overruns, {} dropped, {} lost edges, {} empty — last byte at {}ms", - irqs, - RX_BYTES.load(Ordering::Relaxed), - KBD_EVENTS.load(Ordering::Relaxed), - AUX_EVENTS.load(Ordering::Relaxed), - UNEXPLAINED_N.load(Ordering::Relaxed), - DISCARDS.load(Ordering::Relaxed), - OVERRUNS.load(Ordering::Relaxed), - DROPPED_TOTAL.load(Ordering::Relaxed), - LOST_EDGES.load(Ordering::Relaxed), - counts.empty, - LAST_IRQ_NS.load(Ordering::Relaxed) / 1_000_000 - ); -} - -/// Put the verdict on the panel too, but only when there is no other -/// channel: declines once `serial::has_console()`, mirroring -/// `panic_flush`'s own test. -fn to_screen() { - if crate::drivers::serial::has_console() { - return; - } - crate::drivers::panic_console::boot_checkpoint(); -} - -/// The decoder saw a device reset. Handled on `IRQ_CPU`, whichever CPU noticed. -static AUX_RESET_PENDING: AtomicBool = AtomicBool::new(false); -static AUX_REENABLE_FAILURES: AtomicU32 = AtomicU32::new(0); - -/// Sized for "the controller is gone": a live device acks a one-byte command -/// in well under a millisecond, and this is interrupts-off time. -const AUX_REENABLE: Budget = Budget::of( - Duration::from_millis(30), - "the re-enable handshake is abandoned and counted against AUX_REENABLE_GIVE_UP", -); - -/// Consecutive re-enable failures before the aux line is masked and the -/// pointer written off, rather than retried forever. -const AUX_REENABLE_GIVE_UP: u32 = 3; - -// Single producer, pinned to one CPU behind an interrupt gate so the -// handler cannot nest; Release/Acquire on both indices since a drain can -// run on any CPU. - -const RING_LEN: usize = 256; -const AUX_FLAG: u64 = 1 << 8; -/// Microsecond arrival time in the rest of the slot. The mouse framer -/// resyncs on the gap between adjacent bytes, so batching would flatten it. -const TIME_SHIFT: u32 = 9; - -static BYTES: [AtomicU64; RING_LEN] = [const { AtomicU64::new(0) }; RING_LEN]; -static HEAD: AtomicU32 = AtomicU32::new(0); -static TAIL: AtomicU32 = AtomicU32::new(0); - -fn push_isr(byte: u8, aux: bool, arrived_ns: u64) { - let head = HEAD.load(Ordering::Relaxed); - if head.wrapping_sub(TAIL.load(Ordering::Acquire)) as usize >= RING_LEN { - DROPPED.fetch_add(1, Ordering::Relaxed); - return; - } - let slot = &BYTES[head as usize % RING_LEN]; - let value = ((arrived_ns / 1_000) << TIME_SHIFT) - | if aux { AUX_FLAG } else { 0 } - | byte as u64; - slot.store(value, Ordering::Relaxed); - HEAD.store(head.wrapping_add(1), Ordering::Release); -} - -fn pop() -> Option<(u8, bool, u64)> { - let tail = TAIL.load(Ordering::Relaxed); - if tail == HEAD.load(Ordering::Acquire) { - return None; - } - let value = BYTES[tail as usize % RING_LEN].load(Ordering::Relaxed); - // Counted before the slot releases: a byte is in the ring or in this - // count, never in neither. See [`RX_BYTES`]. - RX_BYTES.fetch_add(1, Ordering::Relaxed); - TAIL.store(tail.wrapping_add(1), Ordering::Release); - Some((value as u8, value & AUX_FLAG != 0, (value >> TIME_SHIFT) * 1_000)) -} - -fn has_bytes() -> bool { - HEAD.load(Ordering::Acquire) != TAIL.load(Ordering::Relaxed) -} - -#[inline] -fn buffer_full(status: u8) -> bool { - status & OBF != 0 -} - -/// Rust half of the pin-interrupt handler. Read the module doc before adding -/// anything to it. -pub extern "sysv64" fn handler() { - crate::arch::percpu::irq_took!(I8042); - let timestamp = crate::clock::nanos_since_boot(); - // No compare-exchange: this handler cannot nest, so there's no second writer. - let first = FIRST_IRQ_NS.load(Ordering::Relaxed) == 0; - if first { - FIRST_IRQ_NS.store(timestamp, Ordering::Relaxed); - } - LAST_IRQ_NS.store(timestamp, Ordering::Relaxed); - let mut n = 0; - while n < ISR_BURST { - let status = inb(port(&STATUS)); - if !buffer_full(status) { - break; - } - // Timestamped per byte, not once for the burst: the mouse framer - // resyncs on the gap between adjacent bytes, and a burst would flatten it. - push_isr(inb(port(&DATA)), status & AUXB != 0, crate::clock::nanos_since_boot()); - n += 1; - } - if n == ISR_BURST && buffer_full(inb(port(&STATUS))) { - QUARANTINE.store(true, Ordering::Relaxed); - } - // Only the first interrupt can be the arming edge (IRR delivers it before - // any later assertion), and it settles the debt either way. - if first && ARM_EDGE_OWED.swap(false, Ordering::Relaxed) && n == 0 { - ARM_EDGE_CONSUMED.store(true, Ordering::Relaxed); - } - // Recorded after the burst, not before: the Release here also publishes - // the bytes above it, so no reader sees the count without the bytes. - TALLY.record(if n == 0 { Carried::Nothing } else { Carried::Bytes }); - if n > 0 { - crate::irq_ring::isr_publish(IrqSource::I8042, timestamp); - crate::preempt::set_need_resched(); - } - crate::arch::apic::eoi(); -} - -/// Bytes a decoder has taken but not yet resolved. Held until the byte that -/// *ends* the run, so a multi-byte undecodable sequence can be named whole -/// rather than by its last byte alone. -struct Partial { - bytes: [u8; UNEXPLAINED_LEN], - len: usize, -} - -impl Partial { - const fn new() -> Self { - Self { bytes: [0; UNEXPLAINED_LEN], len: 0 } - } - - fn push(&mut self, byte: u8) { - if self.len < UNEXPLAINED_LEN { - self.bytes[self.len] = byte; - self.len += 1; - } - } - - /// The run produced an event, so none of it is a suspect. - fn clear(&mut self) { - self.len = 0; - } - - /// The run ended on `last` and produced nothing. All of it is. - fn blame(&mut self, last: u8, aux: bool) { - for i in 0..self.len { - record_unexplained(self.bytes[i], aux); - } - self.len = 0; - record_unexplained(last, aux); - } -} - -struct Decoders { - keys: KeyDecoder, - pointer: MouseDecoder, - kbd_partial: Partial, - aux_partial: Partial, -} - -/// No ISR may lock this: `drain` holds it in thread context, so an ISR -/// taking it here self-deadlocks the CPU. -static PS2: Lock = Lock::new(Decoders { - keys: KeyDecoder::new(), - pointer: MouseDecoder::new(), - kbd_partial: Partial::new(), - aux_partial: Partial::new(), -}); - -/// Turn whatever the ISR published into events and wakes. Runs at the top of -/// every scheduler pass on every CPU, so the idle cost is one atomic load. -pub fn service() { - let recorded = crate::irq_ring::take(IrqSource::I8042).is_some(); - if QUARANTINE.load(Ordering::Relaxed) { - quarantine(); - return; - } - if !ACTIVE.load(Ordering::Relaxed) { - return; - } - // Polled I/O: only `IRQ_CPU` may run this; another CPU leaves the - // request standing until its own next pass. - if AUX_RESET_PENDING.load(Ordering::Relaxed) && is_irq_cpu() { - aux_reenable(); - } - if has_bytes() { - // Asked again with bytes in hand: a record read absent may belong - // to an interrupt that arrived just after that read. - let recorded = recorded || crate::irq_ring::take(IrqSource::I8042).is_some(); - service_bytes(recorded); - } - // Last: reported from the top, the first pass would print "0 bytes". - let health = HEALTH.load(Ordering::Relaxed); - if health != HEALTH_DONE { - report_health(health); - } - report_counters(); -} - -/// Decode what the ISR left in the ring and wake whoever it belongs to. -/// `recorded` is whether this pass found an `irq_ring` record for the source. -fn service_bytes(recorded: bool) { - // Only `IRQ_CPU` can hold a record for this source; counting `!recorded` - // on any other CPU would report a lost edge on every `--smp N>1` boot. - if !recorded && is_irq_cpu() { - // Loud the first time, silent after: nothing reads a rate. - if LOST_EDGES.fetch_add(1, Ordering::Relaxed) == 0 { - log!("i8042: bytes with no IRQ record — an edge was lost"); - } - } - - let Drained { keys, motion, aux_reset } = drain(); - - // Wake only when the decode queued something, or a stray wake parks the - // next reader until the following real event. - let woke_kb = keys > 0; - if woke_kb { - crate::keyboard::WATCH.post(); - } - let woke_ms = motion > 0; - if woke_ms { - crate::mouse::WATCH.post(); - } - - if aux_reset { - AUX_RESET_PENDING.store(true, Ordering::Relaxed); - } -} - -struct Drained { - keys: usize, - motion: usize, - aux_reset: bool, -} - -/// Consume the ring. Releases `PS2` before returning, so the caller's wakes -/// never run under the driver lock. Lock order: PS2 before KEY_BUF, never -/// the reverse. -fn drain() -> Drained { - let mut state = PS2.lock(); - let mut out = Drained { keys: 0, motion: 0, aux_reset: false }; - let mut lost = false; - - let dropped = DROPPED.swap(0, Ordering::Relaxed); - DROPPED_TOTAL.fetch_add(dropped, Ordering::Relaxed); - if dropped > 0 { - // Never expected: 256 slots against ~300 B/s, drained every pass. - log!("i8042: ring overflow, {} bytes dropped — resyncing", dropped); - // A hole in a framed stream: both decoders' partial state is - // meaningless now, and the pointer would stay one byte off forever. - state.keys.reset(); - state.pointer.reset(); - state.kbd_partial.clear(); - state.aux_partial.clear(); - lost = true; - } - - while let Some((byte, aux, arrived)) = pop() { - // Whether the run is over and whether it produced anything — a - // dropped break or a zero-motion packet counts as "nothing" too. - let explained = if aux { - match state.pointer.feed(byte, arrived) { - MouseOutcome::Pending => { - state.aux_partial.push(byte); - continue; - } - MouseOutcome::Packet { buttons, dx, dy } => { - let queued = crate::mouse::handle_motion( - crate::mouse::PointerSource::PS2, - buttons, - crate::mouse::Motion::Relative { dx, dy }, - 0, - ); - if queued { - out.motion += 1; - } - queued - } - MouseOutcome::Reset => { - out.aux_reset = true; - true - } - MouseOutcome::Discarded => { - DISCARDS.fetch_add(1, Ordering::Relaxed); - false - } - } - } else { - match state.keys.feed(byte) { - KeyOutcome::Pending => { - state.kbd_partial.push(byte); - continue; - } - KeyOutcome::Key { usage, pressed } => { - let queued = crate::keyboard::handle_key(usage, pressed); - if queued { - out.keys += 1; - } - queued - } - // Overrun codes explain themselves; counted, not blamed. - KeyOutcome::Lost => { - OVERRUNS.fetch_add(1, Ordering::Relaxed); - lost = true; - true - } - KeyOutcome::None => false, - } - }; - let partial = if aux { &mut state.aux_partial } else { &mut state.kbd_partial }; - if explained { - partial.clear(); - } else { - partial.blame(byte, aux); - } - } - - if lost { - // A held key's break, or the packet lifting a held pointer button, - // may be among what was lost — so every held input is released. - out.keys += crate::keyboard::release_all(); - if crate::mouse::release_buttons(crate::mouse::PointerSource::PS2) { - out.motion += 1; - } - } - - KBD_EVENTS.fetch_add(out.keys as u32, Ordering::Relaxed); - AUX_EVENTS.fetch_add(out.motion as u32, Ordering::Relaxed); - // `RX_BYTES` is not here: `pop` already counted each byte as it took it. - out -} - -/// A controller producing bytes faster than the ISR's bound can drain them. -/// One masked line and a dead keyboard, never a spinning CPU. -fn quarantine() { - QUARANTINE.store(false, Ordering::Relaxed); - ACTIVE.store(false, Ordering::Relaxed); - // The pin is about to be masked, so no health verdict follows this line. - HEALTH.store(HEALTH_DONE, Ordering::Relaxed); - // Force-released: nothing else can lift a held key or pointer button - // once the line is masked. - crate::keyboard::release_all(); - crate::mouse::release_buttons(crate::mouse::PointerSource::PS2); - // The count, not the intent: the log line is only true if the mask took. - let mut masked = 0; - for line in [KEYBOARD_GSI.load(Ordering::Relaxed), AUX_GSI.load(Ordering::Relaxed)] { - if line != u32::MAX && ioapic::set_masked(Gsi(line), true).is_ok() { - masked += 1; - } - } - log!( - "i8042: quarantined — output buffer never emptied, masked={} (kbd={} aux={} lost={})", - masked, - KBD_EVENTS.load(Ordering::Relaxed), - AUX_EVENTS.load(Ordering::Relaxed), - LOST_EDGES.load(Ordering::Relaxed) - ); -} - -// Each read below is done as its section's sole reader: init before the -// vector is armed, and the aux re-enable on `IRQ_CPU` under `IrqGuard::close` -// — so no ISR ever races them. - -fn deadline(millis: u64) -> u64 { - crate::clock::nanos_since_boot() + millis * 1_000_000 -} - -/// Each stage below is a [`Budget`]; expiry names the stage and degrades the -/// probe's answer rather than panicking. -const fn ms(budget: Budget) -> u64 { - budget.duration().millis() -} - -/// Covers every controller-only step outside a named stage (disable, flush, -/// config r/w, both interface tests, the arming write) — none waits on an -/// EC, but the time is still spent. -const CONTROLLER: Budget = Budget::of( - Duration::from_millis(250), - "the stage that ran out is named and the probe reports DISABLED", -); -/// `0xAA`; separates "a controller" from "something else decoding -/// 0x60/0x64" (SMM-trapped ports for USB legacy emulation). -const SELFTEST: Budget = Budget::of( - Duration::from_millis(500), - "the controller is reported absent and the machine boots with no PS/2 input", -); -/// `0xF5`, the `0xF0 0x00` read-back and `0xF4` — each acknowledged by the -/// keyboard, not the controller. -const KEYBOARD: Budget = Budget::of( - Duration::from_millis(750), - "the keyboard stage is named as the one that ran out", -); -/// The aux port's `0xFF` is a device reset, answered with a real self-test: -/// this stage must not be shortened to fix an arithmetic error elsewhere. -const AUX_RESET: Budget = Budget::of( - Duration::from_millis(600), - "the pointer is written off and the keyboard half still comes up", -); - -/// Derived, never written down independently: a literal short of the -/// stages' sum would let a slow-but-real machine exhaust it before the -/// arming write, and that timeout would then present as `DISABLED — cfg … -/// did not take`, a controller fault it is not. -fn init_budget_ms() -> u64 { - ms(CONTROLLER) + ms(SELFTEST) + ms(KEYBOARD) + ms(AUX_RESET) -} - -/// A stage's own deadline, clamped to the probe's; `None` once the probe's -/// own budget is already spent. Names the stage that ran out, since the -/// clamp alone can't tell a slow EC from a broken controller. -fn stage(millis: u64, budget: u64, name: &str) -> Option { - if crate::clock::nanos_since_boot() >= budget { - log!( - "i8042: {}ms init budget spent before the {name} stage — no PS/2 input", - init_budget_ms() - ); - return None; - } - Some(deadline(millis).min(budget)) -} - -fn budget_spent(budget: u64) -> bool { - crate::clock::nanos_since_boot() >= budget -} - -/// The status register, or `None` when nothing decodes the port. -fn status() -> Option { - match inb(port(&STATUS)) { - FLOATING_BUS => None, - other => Some(other), - } -} - -fn wait_writable(deadline: u64) -> bool { - loop { - let Some(status) = status() else { return false }; - if status & IBF == 0 { - return true; - } - if crate::clock::nanos_since_boot() >= deadline { - return false; - } - } -} - -fn read_data(deadline: u64) -> Option { - loop { - if status()? & OBF != 0 { - return Some(inb(port(&DATA))); - } - if crate::clock::nanos_since_boot() >= deadline { - return None; - } - } -} - -fn command(cmd: u8, deadline: u64) -> bool { - wait_writable(deadline) && { - // SAFETY: the status port written is the 8042's command port; `cmd` is - // always one of this module's own `CMD_*` constants, and the - // controller has no path to memory. - unsafe { outb(port(&STATUS), cmd) }; - true - } -} - -fn write_data(byte: u8, deadline: u64) -> bool { - wait_writable(deadline) && { - // SAFETY: the controller's data port; `byte` is a - // config word or device command, neither reaching memory. - unsafe { outb(port(&DATA), byte) }; - true - } -} - -fn read_config(deadline: u64) -> Option { - command(CMD_READ_CONFIG, deadline).then(|| read_data(deadline)).flatten() -} - -fn write_config(value: u8, deadline: u64) -> bool { - command(CMD_WRITE_CONFIG, deadline) && write_data(value, deadline) -} - -/// Iteration-bounded, not clock-bounded: draining 32 times is already past -/// any legitimate backlog, and OBF still set after that will not clear. -fn flush() -> bool { - for _ in 0..32 { - match status() { - None => return false, - Some(s) if s & OBF == 0 => return true, - Some(_) => { - inb(port(&DATA)); - } - } - } - status().is_some_and(|s| s & OBF == 0) -} - -/// Sends a device command byte by byte, each acked with 0xFA. `aux` prefixes -/// every byte with the controller command that redirects it to port 2. -/// No retry on 0xFE (resend): unreachable on QEMU, and a silent retry would -/// hide the one wire-error case worth knowing about. -fn port_command(bytes: &[u8], deadline: u64, aux: bool) -> bool { - let tag = if aux { "aux" } else { "kbd" }; - for &byte in bytes { - if (aux && !command(CMD_WRITE_AUX, deadline)) || !write_data(byte, deadline) { - log!("i8042: {tag} cmd {:#04x} — input buffer never cleared", byte); - return false; - } - match read_data(deadline) { - Some(0xFA) => {} - other => { - log!("i8042: {tag} cmd {:#04x} answered {:?}, not ack", byte, other); - return false; - } - } - } - true -} - -/// The keyboard port, unprefixed. -fn device_command(bytes: &[u8], deadline: u64) -> bool { - port_command(bytes, deadline, false) -} - -/// What `0xF0 0x00` established. -enum SetQuery { - /// The read-back byte, translated like every other byte from port 1. - Told(u8), - /// A non-ack byte came back; the device does not implement the exchange. - Refused(u8), - /// Nothing came back, or the controller never took the write. - Silent, -} - -/// Ask which scancode set the keyboard is in. Read, never write: nothing -/// else in the machine's life sends the matching `0xF0 0x02` either (Linux's -/// `atkbd_select_set`, EDK2's `Ps2KeyboardDxe`), and a write cannot improve -/// on a read that already answers. -fn query_scancode_set(deadline: u64) -> SetQuery { - if !write_data(0xF0, deadline) { - return SetQuery::Silent; - } - match read_data(deadline) { - Some(0xFA) => {} - Some(other) => return SetQuery::Refused(other), - None => return SetQuery::Silent, - } - if !write_data(0x00, deadline) { - return SetQuery::Silent; - } - // A device may ack the command byte and then refuse the argument. - match read_data(deadline) { - Some(0xFA) => {} - Some(other) => return SetQuery::Refused(other), - None => return SetQuery::Silent, - } - match read_data(deadline) { - Some(set) => SetQuery::Told(set), - None => SetQuery::Silent, - } -} - -/// Same as `port_command`, prefixed for port 2. -fn aux_command(bytes: &[u8], deadline: u64) -> bool { - port_command(bytes, deadline, true) -} - -/// Re-enables data reporting after the device resets itself. Lines are left -/// unmasked: masking stops neither an executing ISR nor a latched vector, -/// and drops an edge on a masked entry outright. -fn aux_reenable() { - AUX_RESET_PENDING.store(false, Ordering::Relaxed); - let ok = { - let _irq = crate::arch::IrqGuard::close(); - let budget = deadline(ms(AUX_REENABLE)); - // Masking the line doesn't stop the device: port 1 is disabled so a - // stray keystroke mid-handshake can't be consumed as the aux ack. - command(CMD_DISABLE_PORT1, budget); - let ok = aux_command(&[0xF4], budget); - command(CMD_ENABLE_PORT1, budget); - // Edge delivery: a byte left in OBF means no further interrupt ever. - handler_poll(); - ok - }; - if ok { - AUX_REENABLE_FAILURES.store(0, Ordering::Relaxed); - log!("i8042: aux reset itself, reporting re-enabled"); - return; - } - let failures = AUX_REENABLE_FAILURES.fetch_add(1, Ordering::Relaxed) + 1; - if failures < AUX_REENABLE_GIVE_UP { - log!("i8042: aux reset itself, re-enable failed ({failures}/{AUX_REENABLE_GIVE_UP})"); - return; - } - let aux = AUX_GSI.swap(u32::MAX, Ordering::Relaxed); - if aux != u32::MAX { - let _ = ioapic::set_masked(Gsi(aux), true); - } - crate::mouse::release_buttons(crate::mouse::PointerSource::PS2); - log!("i8042: aux re-enable failed {failures} times — pointer written off, line masked"); -} - -/// What firmware claims about the 8042 — never what decides. -fn firmware_claim(rsdp_addr: u64) -> Result<(u8, u16), crate::drivers::acpi::TableError> { - crate::drivers::acpi::iapc_boot_arch(rsdp_addr) -} - -pub fn init(rsdp_addr: u64) { - if crate::actuator::i8042_withheld() { - log!("i8042: withheld, left unprobed for a claim"); - return; - } - // Declared before the first access, and for the rest of the boot: a - // controller this probe has touched is no process's to claim. - for (run, port) in [(&DATA, 0x60), (&STATUS, 0x64)] { - match pio::declare("the i8042", toyos_userbound::Ports::one(port), toyos_userbound::Mediated::Kept) { - Ok(declared) => run.set(declared), - Err(why) => { - log!("i8042: port {port:#x} not declared ({why:?}) — left unprobed"); - return; - } - } - } - // Logged, never obeyed: bit 1 is one summary bit, while the handshake - // below is three direct observations of the machine in front of us. - match firmware_claim(rsdp_addr) { - Ok((revision, flags)) => log!( - "i8042: FADT rev {} iapc_boot_arch={:#06x}, bit 1 (8042) {} — probing either way", - revision, - flags, - if flags & FADT_8042 != 0 { "set" } else { "clear" } - ), - Err(e) => log!("i8042: no trustworthy FADT ({e:?}) — firmware claims nothing either way"), - } - - // One `inb` settles every machine that has nothing there, before a single - // byte is written to ports that might belong to something else. - if status().is_none() { - log!("i8042: absent — port 0x64 reads {FLOATING_BUS:#04x}, nothing decodes it"); - return; - } - - // The whole probe's budget: the sum of every stage plus the steps - // between them, so no machine can spend it before the last stage has its own. - let budget = deadline(init_budget_ms()); - - // Firmware may leave scanning on. A keystroke arriving mid-handshake - // makes the config read return a scancode and everything after garbage. - command(CMD_DISABLE_PORT1, budget); - command(CMD_DISABLE_AUX, budget); - - if !flush() { - log!("i8042: absent (output buffer never drains)"); - return; - } - - let Some(before) = read_config(budget) else { - log!("i8042: absent (no config byte)"); - return; - }; - // IRQs off until the device answers; translate on, since set 1 is what - // this kernel decodes; port-1 clock on. - let wanted = (before & !(CFG_PORT1_IRQ | CFG_PORT2_IRQ | CFG_PORT1_CLOCK_OFF)) | CFG_TRANSLATE; - if !write_config(wanted, budget) { - log!("i8042: absent (config write never accepted)"); - return; - } - match read_config(budget) { - Some(v) if v == wanted => {} - other => { - log!("i8042: absent (cfg wrote {:#04x}, read back {:?})", wanted, other); - return; - } - } - - let Some(selftest_deadline) = stage(ms(SELFTEST), budget, "self-test") else { - return; - }; - command(CMD_SELF_TEST, selftest_deadline); - match read_data(selftest_deadline) { - Some(0x55) => {} - other => { - log!( - "i8042: absent (self-test {:?}, {}ms) — no PS/2 input", - other, - ms(SELFTEST) - ); - return; - } - } - // Some controllers reset the config byte across 0xAA. - write_config(wanted, budget); - - command(CMD_TEST_PORT1, budget); - let port1 = read_data(budget); - if port1 != Some(0x00) { - log!("i8042: port 1 interface test {:?} — no keyboard", port1); - return; - } - // Enabling port 2 clears its clock-disable bit iff the port exists; the - // interface test is the cheap way to learn it does not. - command(CMD_ENABLE_AUX, budget); - let dual = read_config(budget).is_some_and(|c| c & CFG_PORT2_CLOCK_OFF == 0); - command(CMD_DISABLE_AUX, budget); - let port2 = dual && { - command(CMD_TEST_AUX, budget); - read_data(budget) == Some(0x00) - }; - - command(CMD_ENABLE_PORT1, budget); - log!( - "i8042: ok selftest=0x55 cfg={:#04x}->{:#04x} port1=ok port2={}", - before, - wanted, - if port2 { "ok" } else if dual { "failed" } else { "absent" } - ); - - // The slowest step on a real EC, hence its own budget. - let Some(kbd) = stage(ms(KEYBOARD), budget, "keyboard") else { - return; - }; - if !device_command(&[0xF5], kbd) { - log!("i8042: kbd would not stop scanning — disabled"); - return; - } - // The controller translates the reply too, so the read-back names the - // wire format outright; refusing to decode a format we did not ask for - // beats typing nonsense on a machine we cannot single-step. - let (wire, how) = match query_scancode_set(kbd) { - SetQuery::Told(0x41) => ("set2+xlat", "readback 0x41"), - SetQuery::Told(0x01) => ("set1 raw", "readback 0x01, translation not applied"), - SetQuery::Told(0x43) => { - log!("i8042: kbd DISABLED — readback 0x43 means set 1 through the set2 table"); - return; - } - SetQuery::Told(0x02) => { - log!("i8042: kbd DISABLED — readback 0x02 means set 2 raw on the wire"); - return; - } - SetQuery::Told(other) => { - log!("i8042: kbd DISABLED — readback {:#04x} names no known wire format", other); - return; - } - SetQuery::Refused(byte) if before & CFG_TRANSLATE != 0 => { - log!( - "i8042: kbd will not report its scancode set (0xF0 0x00 answered {:#04x}); firmware's own cfg {:#04x} has translate on, so the wire is set 1", - byte, - before - ); - ("set2+xlat", "assumed, the set query was refused") - } - SetQuery::Refused(byte) => { - log!( - "i8042: kbd DISABLED - the set query answered {:#04x} and firmware's cfg {:#04x} has translate off, so nothing says what the wire carries", - byte, - before - ); - return; - } - SetQuery::Silent => { - log!("i8042: kbd DISABLED - the 0xF0 0x00 set query did not complete"); - return; - } - }; - if !device_command(&[0xF4], kbd) { - log!("i8042: kbd would not resume scanning — disabled"); - return; - } - - // The TrackPoint. Failure here costs the pointer and nothing else, so - // every step logs and falls through rather than returning. - let aux = port2 && { - command(CMD_ENABLE_AUX, budget); - stage(ms(AUX_RESET), budget, "aux reset").is_some_and(|reset| { - // 0xFF answers 0xFA, then 0xAA (BAT ok), then the device id. - aux_command(&[0xFF], reset) - && read_data(reset) == Some(0xAA) - && read_data(reset).is_some() - && aux_command(&[0xF2], reset) - && { - let id = read_data(reset); - if id != Some(0x00) { - log!("i8042: aux id {:?}, not a plain 3-byte mouse — framing anyway", id); - } - true - } - // No IntelliMouse knock: a fixed 3-byte frame is what makes - // resync trivially self-healing. - && aux_command(&[0xF3, 0x64], reset) - && aux_command(&[0xE8, 0x03], reset) - && aux_command(&[0xF4], reset) - }) - }; - if port2 && !aux { - log!("i8042: aux init failed — no pointer"); - command(CMD_DISABLE_AUX, budget); - } - - // Steps above leave residue in the output buffer. - flush(); - - let apic_id = crate::arch::apic::id(); - let Some(kbd_line) = ioapic::gsi_for_isa_irq(ISA_IRQ_KEYBOARD) else { - log!("i8042: no I/O APIC covers IRQ 1 — keyboard cannot be routed"); - return; - }; - // `route` refuses rather than mis-route: a keyboard-less boot is - // diagnosable, an interrupt delivered to the wrong CPU is not. - if let Err(e) = ioapic::route(kbd_line.gsi, I8042_VECTOR, apic_id, kbd_line.trigger, kbd_line.polarity) - { - log!("i8042: GSI {} not routable to apic {}: {:?}", kbd_line.gsi.0, apic_id, e); - return; - } - KEYBOARD_GSI.store(kbd_line.gsi.0, Ordering::Relaxed); - // `apic_id` is this CPU's: everything downstream that says "the pinned - // CPU" reads it from here. - IRQ_CPU.store(crate::arch::percpu::cpu_id(), Ordering::Relaxed); - - let aux_line = aux.then(|| ioapic::gsi_for_isa_irq(ISA_IRQ_AUX)).flatten().filter(|l| { - match ioapic::route(l.gsi, I8042_VECTOR, apic_id, l.trigger, l.polarity) { - Ok(()) => true, - Err(e) => { - log!("i8042: GSI {} not routable to apic {}: {:?}", l.gsi.0, apic_id, e); - false - } - } - }); - if let Some(l) = aux_line { - AUX_GSI.store(l.gsi.0, Ordering::Relaxed); - } - - // Interrupts off while arming: a byte landing between the last flush and - // the unmask would sit in OBF forever otherwise (edge delivery doesn't - // re-assert until read). - crate::arch::cpu::disable_interrupts(); - let mut config = wanted | CFG_PORT1_IRQ; - if aux_line.is_some() { - // `wanted` has port 2 disabled (from firmware's config); writing it - // back unchanged would undo the 0xA8 enable above. - config = (config | CFG_PORT2_IRQ) & !CFG_PORT2_CLOCK_OFF; - } - // The read-back may not be skipped: a controller that drops the write - // still fills the output buffer and never asserts, so nothing else - // downstream can tell. - let wrote = write_config(config, budget); - let readback = read_config(budget); - if !wrote || readback != Some(config) { - crate::arch::cpu::enable_interrupts(); - // A budget spent upstream makes this write and its read-back give up - // instantly, indistinguishable from a dropped write — named explicitly. - if budget_spent(budget) { - log!( - "i8042: DISABLED — the {}ms init budget was spent before the pin could be armed; this is a timeout, not a controller fault", - init_budget_ms() - ); - } else { - match readback { - Some(v) => log!( - "i8042: DISABLED — cfg {:#04x} did not take (read back {:#04x}); the pin would never assert", - config, - v - ), - None => log!( - "i8042: DISABLED — cfg {:#04x} did not take (no config byte came back); the pin would never assert", - config - ), - } - } - return; - } - // The read-back consumed its response with the IRQ bits live, so its edge is owed. - ARM_EDGE_OWED.store(true, Ordering::Relaxed); - let unmasked = ioapic::set_masked(kbd_line.gsi, false).is_ok(); - // Captured, not discarded: an aux GSI that won't unmask is the - // TrackPoint silently dead on a boot that otherwise reads green. - let aux_unmasked = aux_line.is_some_and(|l| ioapic::set_masked(l.gsi, false).is_ok()); - ACTIVE.store(true, Ordering::Relaxed); - crate::keyboard::declare_source(); - if aux_line.is_some() { - crate::mouse::declare_source(); - } - ARMED_NS.store(crate::clock::nanos_since_boot(), Ordering::Relaxed); - HEALTH.store(HEALTH_ARMED, Ordering::Relaxed); - handler_poll(); - crate::arch::cpu::enable_interrupts(); - - log!( - "i8042: kbd {} ({}) scanning on, GSI {} -> vec {:#04x} apic {} {}", - wire, - how, - kbd_line.gsi.0, - I8042_VECTOR, - apic_id, - if unmasked { "on" } else { "MASKED" } - ); - match aux_line { - Some(l) => log!( - "i8042: aux rate=100 res=8/mm, GSI {} -> vec {:#04x} apic {} {}", - l.gsi.0, - I8042_VECTOR, - apic_id, - if aux_unmasked { "on" } else { "MASKED" } - ), - None => log!("i8042: no pointer on the aux port"), - } - -} - -/// The handler's drain loop, without the EOI. Runs with interrupts off on -/// `IRQ_CPU`, keeping `push_isr`'s producer single. Publishes the same -/// record the ISR does — a silent push here would manufacture a lost edge. -fn handler_poll() { - let timestamp = crate::clock::nanos_since_boot(); - let mut n = 0; - while n < ISR_BURST { - let status = inb(port(&STATUS)); - if status & OBF == 0 { - break; - } - push_isr(inb(port(&DATA)), status & AUXB != 0, crate::clock::nanos_since_boot()); - n += 1; - } - if n > 0 { - crate::irq_ring::isr_publish(IrqSource::I8042, timestamp); - crate::preempt::set_need_resched(); - } -} diff --git a/kernel/src/arch/x86_64/i8042/tally.rs b/kernel/src/arch/x86_64/i8042/tally.rs deleted file mode 100644 index fb9470dc6b7..00000000000 --- a/kernel/src/arch/x86_64/i8042/tally.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Interrupt counts packed into one `u64`, updated with a single atomic op, so a read always sees carried and empty as of the same instant. -//! No `crate::` references: `kernel-loom` compiles this file under `feature = "loom"` to drive the real word in its tests. - -#[cfg(not(feature = "loom"))] -use core::sync::atomic::{AtomicU64, Ordering}; - -#[cfg(feature = "loom")] -use loom::sync::atomic::{AtomicU64, Ordering}; - -/// What one interrupt turned out to be, decided by the ISR after its burst. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Carried { - /// The burst read at least one byte, all reached the ring before this was recorded. - Bytes, - /// OBF was already clear: the driver's polling init had taken the byte before the interrupt arrived. - Nothing, -} - -/// A carried/empty pair read at one instant; built only by [`Tally::read`]. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Counts { - /// Interrupts that delivered at least one byte into the ring. - pub carried: u32, - /// Interrupts the ISR found nothing behind. - pub empty: u32, -} - -impl Counts { - /// Total interrupts observed, saturating so it can't disagree with `carried + empty`. - pub fn irqs(self) -> u32 { - self.carried.saturating_add(self.empty) - } -} - -// The two halves are disjoint: an interrupt increments exactly one of them. -const CARRIED_ONE: u64 = 1; -const EMPTY_ONE: u64 = 1 << 32; - -pub struct Tally { - packed: AtomicU64, -} - -impl Tally { - // Must stay `const`: `Tally` is a kernel `static`, and loom's atomics have no const constructor. - #[cfg(not(feature = "loom"))] - pub const fn new() -> Self { - Self { packed: AtomicU64::new(0) } - } - - // No `Default`: it can't be `const`, and the const arm above is what the `static` needs. - #[allow(clippy::new_without_default)] - #[cfg(feature = "loom")] - pub fn new() -> Self { - Self { packed: AtomicU64::new(0) } - } - - /// Account for one interrupt. Call once, from the ISR, after the burst — never on the way in. - pub fn record(&self, carried: Carried) { - // Sole writer: the ISR is pinned to one CPU, so this load-then-add can't race itself. - let counts = Self::split(self.packed.load(Ordering::Relaxed)); - // Saturate rather than wrap: wrapping the low half would carry into the high half. - if counts.carried == u32::MAX || counts.empty == u32::MAX { - return; - } - let one = match carried { - Carried::Bytes => CARRIED_ONE, - Carried::Nothing => EMPTY_ONE, - }; - self.packed.fetch_add(one, Ordering::Release); - } - - /// The pair as it stood at one instant; the acquire pairs with `record`'s release, so a reader that sees a count sees the bytes behind it. - pub fn read(&self) -> Counts { - Self::split(self.packed.load(Ordering::Acquire)) - } - - fn split(packed: u64) -> Counts { - Counts { carried: packed as u32, empty: (packed >> 32) as u32 } - } -} diff --git a/kernel/src/arch/x86_64/idt/i8042.rs b/kernel/src/arch/x86_64/idt/i8042.rs deleted file mode 100644 index e6e223ef52f..00000000000 --- a/kernel/src/arch/x86_64/idt/i8042.rs +++ /dev/null @@ -1,6 +0,0 @@ -use super::device_irq::device_irq_entry; - -device_irq_entry! { - /// The kernel's only reader of port 0x60 for both PS/2 lines. - pub(super) fn i8042_entry => crate::arch::i8042::handler -} diff --git a/kernel/src/arch/x86_64/idt/mod.rs b/kernel/src/arch/x86_64/idt/mod.rs index f78b053f615..684e4af299f 100644 --- a/kernel/src/arch/x86_64/idt/mod.rs +++ b/kernel/src/arch/x86_64/idt/mod.rs @@ -2,7 +2,6 @@ pub(crate) mod exceptions; mod device_irq; mod dma_fault; mod hda; -mod i8042; mod isa; mod kick; mod nmi; @@ -29,9 +28,6 @@ const PIC1_DATA: Port = PIC_PRIMARY.port(1); const PIC2_CMD: Port = PIC_SECONDARY.port(0); const PIC2_DATA: Port = PIC_SECONDARY.port(1); -/// The vector both PS/2 lines are routed to. -pub const I8042_VECTOR: u8 = Vector::I8042 as u8; - /// The vector `apic::kick_cpu` raises. pub const KICK_VECTOR: u8 = Vector::Kick as u8; @@ -255,7 +251,6 @@ idt_vectors! { Nmi = 0x02, nmi::nmi_entry, ist 2; Timer = 0x20, timer::timer_entry; Xhci = 0x21, xhci::xhci_entry; - I8042 = 0x24, i8042::i8042_entry; DmaFault = 0x25, dma_fault::dma_fault_entry; Hda = 0x26, hda::hda_entry; // One per `pcidev` claim slot: the vector is how the kernel knows diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs index f7e45b8dbb6..9946363628e 100644 --- a/kernel/src/arch/x86_64/mod.rs +++ b/kernel/src/arch/x86_64/mod.rs @@ -5,9 +5,9 @@ //! [`percpu`] owns every `gs:` access; nothing outside this directory writes one. //! //! The PC platform's own devices live here too, because no other architecture -//! has them: the i8042, the I/O APIC, the CMOS RTC, the chipset's TCO -//! watchdog, VT-d and AMD-Vi. Generic code reaches each through the concept it serves -//! (`keyboard_controller`, `watchdog`, `iommu_unit`, …), never by its name. +//! has them: the I/O APIC, the CMOS RTC, the chipset's TCO watchdog, VT-d and +//! AMD-Vi. Generic code reaches each through the concept it serves +//! (`watchdog`, `iommu_unit`, …), never by its name. pub mod acpi_mode; pub mod amdvi; @@ -24,7 +24,6 @@ pub mod entry; pub mod fpu; pub mod hpet; pub mod hw; -pub mod i8042; pub mod idt; pub mod iommu_unit; pub mod ioapic; @@ -45,7 +44,6 @@ pub mod vtd; pub mod watchdog; pub use apic as irqchip; -pub use i8042 as keyboard_controller; pub use idt as trap; pub use iommu_unit::interrupt as message_unit; diff --git a/kernel/src/device.rs b/kernel/src/device.rs index ae655b5c95d..4b17387a571 100644 --- a/kernel/src/device.rs +++ b/kernel/src/device.rs @@ -139,8 +139,6 @@ impl Drop for Claim { } pub fn set_framebuffer_info(screen: Screen) { - // Mouse motion maps into a fixed 0..32767 space scaled by this geometry, so it must track every mode change. - crate::mouse::set_screen(screen.info.width, screen.info.height); *FB_INFO.lock() = Some(screen); } @@ -180,20 +178,12 @@ pub fn try_claim(class: DeviceType, selector: [u64; 2]) -> Result { - // A claim is evidence: with no i8042 keyboard armed and no xHCI - // controller bound, nothing can ever feed the stream it names. - if !keyboard::source_exists() { - return Err(ClaimError::Absent); - } let claim = Claim::acquire(class)?; // Keystrokes queued before this claim belong to no one; delivering them would leak them to the new owner. keyboard::discard_queued(); Ok(DeviceClaim::new(class, DeviceInfo::Events, claim)) } DeviceType::Mouse => { - if !mouse::source_exists() { - return Err(ClaimError::Absent); - } let claim = Claim::acquire(class)?; mouse::discard_queued(); Ok(DeviceClaim::new(class, DeviceInfo::Events, claim)) diff --git a/kernel/src/drivers/acpi.rs b/kernel/src/drivers/acpi.rs index dbc02af51d8..e41b4e93a65 100644 --- a/kernel/src/drivers/acpi.rs +++ b/kernel/src/drivers/acpi.rs @@ -165,12 +165,6 @@ pub fn ecam_windows(rsdp_addr: u64) -> Vec { windows } -/// FADT revision and the IA-PC boot architecture flags. -// `Err` is not "absent" and must not be treated as one by the caller. -pub fn iapc_boot_arch(rsdp_addr: u64) -> Result<(u8, u16), TableError> { - toyos_acpi::iapc_boot_arch(direct_phys(), rsdp_addr) -} - /// Which CMOS register holds the RTC's century, as the FADT names it. // `Ok(None)` is "no century register", distinct from `Err`, which the caller must not treat as one. pub fn rtc_century_register(rsdp_addr: u64) -> Result, TableError> { diff --git a/kernel/src/drivers/xhci/device.rs b/kernel/src/drivers/xhci/device.rs index 54c97bfc2e0..aef6bdef40d 100644 --- a/kernel/src/drivers/xhci/device.rs +++ b/kernel/src/drivers/xhci/device.rs @@ -760,7 +760,7 @@ fn bind_hid( let report_size = pointer.map_or(keyboard::REPORT, Pointer::request) as u32; let role = match pointer { None => HidRole::Keyboard(keyboard::Keyboard::default()), - // A pointer with no free button-table entry can't be bound; sharing one would publish another's releases. + // A pointer with no free source can't be bound; sharing one would publish another's releases. Some(pointer) => match crate::mouse::PointerSource::claim() { Some(source) => HidRole::Pointer(source, pointer), None => { diff --git a/kernel/src/drivers/xhci/hid.rs b/kernel/src/drivers/xhci/hid.rs index 9735acf5490..64b5c39c3a8 100644 --- a/kernel/src/drivers/xhci/hid.rs +++ b/kernel/src/drivers/xhci/hid.rs @@ -79,7 +79,10 @@ impl HidDevice { Err(why) => return self.refuse(format_args!("{why:?}")), }, HidRole::Pointer(source, pointer) => match pointer.decode(delivered) { - Ok(read) => mouse::handle_motion(*source, read.buttons, motion(read.motion), read.wheel), + Ok(read) => { + mouse::report(*source, read.buttons, motion(read.motion), read.wheel); + true + } Err(why) => return self.refuse(format_args!("{why:?}")), }, }; @@ -105,12 +108,15 @@ impl HidDevice { } /// Releases everything this device was holding, on its way off the bus. - /// Its own keys, not `keyboard::release_all`: only its `Keyboard` records which held keys are this device's. + /// Its own keys: only its `Keyboard` records which held keys are this device's. pub fn unbind(&mut self) { let queued = match &mut self.role { HidRole::Keyboard(keys) => keyboard::apply(keys.release()) != 0, - // Also frees this device's button-table entry, so replugging costs the machine nothing. - HidRole::Pointer(source, _) => mouse::unbind(*source), + // Also frees this device's source, so replugging costs the machine nothing. + HidRole::Pointer(source, _) => { + mouse::unbind(*source); + true + } }; if queued { self.wake(); diff --git a/kernel/src/drivers/xhci/wait/boot.rs b/kernel/src/drivers/xhci/wait/boot.rs index 4f1843f7582..909e1f0f265 100644 --- a/kernel/src/drivers/xhci/wait/boot.rs +++ b/kernel/src/drivers/xhci/wait/boot.rs @@ -150,9 +150,6 @@ pub fn init(devices: &[PciDevice]) { let hid: usize = controllers.iter().map(|c| c.devices.len()).sum(); log!("xHCI: {} controller(s), {} HID device(s)", controllers.len(), hid); log!("usb-storage: {} device(s)", storage_count()); - // Hot-plug can put either input class on any bound controller. - crate::keyboard::declare_source(); - crate::mouse::declare_source(); // Before the lock takes them: from here every reset this kernel performs // stops these controllers, and it reads them out of `stop`'s atomics rather // than out of `XHCI`, because a panicked CPU may take no lock. diff --git a/kernel/src/input_merge_test.rs b/kernel/src/input_merge_test.rs deleted file mode 100644 index d5c50049ffb..00000000000 --- a/kernel/src/input_merge_test.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! Exercises the keyboard/pointer merge that QEMU cannot: only one input -//! handler per device class is ever live in a guest. A mismatch panics — -//! nothing here crossed a trust boundary. - -use crate::keyboard::{self, RawKeyEvent, MOD_RELEASED, MOD_SHIFT}; -use crate::mouse::{self, Motion, PointerSource}; - -fn next_key(what: &str) -> RawKeyEvent { - keyboard::try_read_event().unwrap_or_else(|| panic!("input-merge: no event for {what}")) -} - -fn drain() { - while keyboard::try_read_event().is_some() {} - while mouse::try_read_event().is_some() {} -} - -pub fn run() { - drain(); - - // The kernel reports MOD_SHIFT only; toyos-keymap turns it into a capital. - assert!(keyboard::handle_key(0xE1, true), "input-merge: Shift down queued nothing"); - assert!(keyboard::handle_key(0x04, true), "input-merge: 'a' down queued nothing"); - assert!(keyboard::handle_key(0xE1, false), "input-merge: Shift up queued nothing"); - // A repeated make (no intervening break) must queue nothing. - assert!(!keyboard::handle_key(0x04, true), "input-merge: repeat make queued an event"); - - let shift_down = next_key("Shift down"); - assert!( - shift_down.keycode == 0xE1 && shift_down.modifiers & MOD_RELEASED == 0, - "input-merge: {:#x} mods {:#x} is not Shift down", - shift_down.keycode, - shift_down.modifiers - ); - let letter = next_key("'a' down"); - assert_eq!( - (letter.keycode, letter.modifiers), - (0x04, MOD_SHIFT), - "input-merge: 'a' down should be usage 0x04 with exactly the other keyboard's Shift set" - ); - let shift_up = next_key("Shift up"); - assert!( - shift_up.keycode == 0xE1 && shift_up.modifiers & MOD_RELEASED != 0, - "input-merge: {:#x} mods {:#x} is not Shift up", - shift_up.keycode, - shift_up.modifiers - ); - - // release_all must clear a key a reset left down, or a modifier sticks for the boot. - assert_eq!(keyboard::release_all(), 1, "input-merge: release_all missed the held key"); - assert!( - next_key("release_all").modifiers & MOD_RELEASED != 0, - "input-merge: release_all queued a press" - ); - assert_eq!(keyboard::modifiers(), 0, "input-merge: a modifier survived release_all"); - - drain(); - - // Publishing a second pointer's buttons verbatim released the first pointer's held button. - let tablet = PointerSource::claim().expect("input-merge: no button-table entry for the tablet"); - let usb_mouse = PointerSource::claim().expect("input-merge: no button-table entry for the mouse"); - assert!(tablet != usb_mouse, "input-merge: two pointers claimed one source"); - assert!( - mouse::handle_motion(PointerSource::PS2, 1, Motion::Relative { dx: 1, dy: 0 }, 0), - "input-merge: PS/2 motion queued nothing" - ); - assert!( - mouse::handle_motion(tablet, 0, Motion::Absolute { x: 100, y: 100 }, 0), - "input-merge: tablet motion queued nothing" - ); - assert_eq!( - last_button_state(), - 1, - "input-merge: a tablet report with no buttons released the button the other pointer holds" - ); - - // Two pointers on the same bus must not alias to one slot. - assert!( - mouse::handle_motion(tablet, 2, Motion::Absolute { x: 101, y: 100 }, 0), - "input-merge: tablet button queued nothing" - ); - assert!( - mouse::handle_motion(usb_mouse, 0, Motion::Relative { dx: 1, dy: 0 }, 0), - "input-merge: second USB pointer queued nothing" - ); - assert_eq!( - last_button_state(), - 3, - "input-merge: one USB pointer's empty report cleared another USB pointer's button" - ); - - // release_buttons is the only way to clear a source quarantined with a button held. - assert!(mouse::release_buttons(tablet), "input-merge: releasing a held source queued nothing"); - assert_eq!(last_button_state(), 1, "input-merge: the tablet's button survived its release"); - assert!( - !mouse::release_buttons(usb_mouse), - "input-merge: releasing a source that held nothing queued an event" - ); - assert!(mouse::release_buttons(PointerSource::PS2), "input-merge: PS/2 release queued nothing"); - assert_eq!(last_button_state(), 0, "input-merge: a button survived every release"); - - drain(); - - // unbind must give the entry back for reuse, or every plug cycle leaks one of 255 sources. - assert!( - mouse::handle_motion(usb_mouse, 1, Motion::Relative { dx: 1, dy: 0 }, 0), - "input-merge: the pointer about to be unplugged queued nothing" - ); - assert_eq!(last_button_state(), 1, "input-merge: its button did not reach the merge"); - assert!( - mouse::unbind(usb_mouse), - "input-merge: unbinding a pointer holding a button published no release" - ); - assert_eq!( - last_button_state(), - 0, - "input-merge: an unplugged pointer's button survived it — every other pointer's motion \ - republishes it, which is a compositor stuck in a drag" - ); - let replug = PointerSource::claim().expect("input-merge: no entry for the pointer plugged back in"); - assert_eq!( - replug, usb_mouse, - "input-merge: a pointer plugged back in took a fresh entry rather than the one its \ - predecessor gave back" - ); - // Reuse must take the released entry, never one still in use. - assert!(tablet != replug, "input-merge: the entry a live pointer holds was handed out again"); - - drain(); - - // Relative motion accumulates in a square space the compositor maps by width and - // height, so pixels-per-count must differ per axis or the pointer skews by the aspect ratio. - for (w, h) in [(1920u32, 1200u32), (1024, 768), (3840, 2160), (1200, 1920), (800, 800)] { - let (sx, sy) = mouse::rel_scale_for(w, h); - assert!(sx >= 1 && sy >= 1, "input-merge: {w}x{h} scaled a count to nothing"); - let across = sx as u64 * w as u64; - let down = sy as u64 * h as u64; - // Each scale is a truncated integer, so each product carries under one - // screen dimension of error. - assert!( - across.abs_diff(down) < (w + h) as u64, - "input-merge: {w}x{h} moves {across} across and {down} down per count" - ); - } - - log!("input-merge: ok"); -} - -/// The buttons the last queued pointer event published, draining the queue. -fn last_button_state() -> u8 { - let mut last = None; - while let Some(ev) = mouse::try_read_event() { - last = Some(ev); - } - last.expect("input-merge: no pointer event").buttons -} diff --git a/kernel/src/irq_census.rs b/kernel/src/irq_census.rs index ec55b603ab9..1ad58e4ce75 100644 --- a/kernel/src/irq_census.rs +++ b/kernel/src/irq_census.rs @@ -26,8 +26,6 @@ pub enum Source { /// claim an interrupt belonged to is the claim's own record, and the census /// is about this machine's interrupt routing. UserDev, - /// Vector 0x24, the i8042's I/O APIC pin — both PS/2 lines. - I8042, /// Vector 0x25, the remapping unit's fault event. DmaFault, /// Vector 0x26, HDA stream completion. @@ -45,11 +43,11 @@ pub enum Source { } impl Source { - pub const COUNT: usize = 11; + pub const COUNT: usize = 10; /// Order `tests/toyos.rs`'s `irq_census_conservation` parses back; must match variant order. pub const NAMES: [&'static str; Self::COUNT] = [ - "timer", "kick", "xhci", "userdev", "i8042", "dmafault", "hda", "tlb", "nmi", + "timer", "kick", "xhci", "userdev", "dmafault", "hda", "tlb", "nmi", "spurious", "unclaimed", ]; } diff --git a/kernel/src/irq_ring.rs b/kernel/src/irq_ring.rs index 3cb80e46fa1..415f4612e8c 100644 --- a/kernel/src/irq_ring.rs +++ b/kernel/src/irq_ring.rs @@ -14,17 +14,15 @@ use crate::scheduler::MAX_CPUS; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum IrqSource { Xhci = 2, - I8042 = 3, } impl IrqSource { - pub const COUNT: usize = 2; + pub const COUNT: usize = 1; /// This source's record in a CPU's slots. const fn slot(self) -> usize { match self { Self::Xhci => 0, - Self::I8042 => 1, } } } diff --git a/kernel/src/keyboard.rs b/kernel/src/keyboard.rs index addaad88910..d91b0ffc894 100644 --- a/kernel/src/keyboard.rs +++ b/kernel/src/keyboard.rs @@ -1,106 +1,42 @@ -//! The machine's single logical keyboard: every driver's key transitions -//! land here as a HID usage, a direction and the modifier mask across every -//! keyboard. Layout, dead keys and escape sequences are `toyos-keymap`'s, in -//! userland. +//! The kernel's keyboard stream: each USB keyboard's key transitions, as a +//! HID usage and a direction, queued as they come for the claim's holder. +//! +//! **One source to the owner of input, merged nowhere here.** Two keyboards +//! holding one usage queue two presses; the owner keeps this stream's held +//! set and every other source's (`toyos_input::Merge`), and makes the +//! modifier mask. Layout, dead keys and escape sequences are +//! `toyos-keymap`'s, in userland. use alloc::collections::VecDeque; use crate::sync::Lock; use crate::watch::Watch; -pub use toyos_abi::input::{RawKeyEvent, MOD_SHIFT, MOD_CTRL, MOD_ALT, MOD_GUI, MOD_RELEASED}; +pub use toyos_abi::input::{RawKeyEvent, MOD_RELEASED}; static KEY_BUF: Lock> = Lock::new(VecDeque::new()); -/// Whether any driver that can ever feed this stream exists — the i8042's -/// keyboard armed, or an xHCI controller bound (hot-plug). A claim's evidence. -static SOURCE_EXISTS: core::sync::atomic::AtomicBool = - core::sync::atomic::AtomicBool::new(false); - -pub fn declare_source() { - SOURCE_EXISTS.store(true, core::sync::atomic::Ordering::Relaxed); -} - -pub fn source_exists() -> bool { - SOURCE_EXISTS.load(core::sync::atomic::Ordering::Relaxed) -} /// What a keyboard read and a keyboard poll wait on. pub static WATCH: Watch = Watch::new(); /// How many transitions the kernel holds for a reader that is not reading; the oldest is dropped on overflow. pub const MAX_QUEUED_EVENTS: usize = 512; -/// Which HID usages are down, one bit each, across every keyboard; keyed by usage, so releasing one keyboard's modifier drops it even if another still holds it. -static HELD: Lock<[u64; 4]> = Lock::new([0; 4]); - -fn is_held(held: &[u64; 4], usage: u8) -> bool { - held[usage as usize / 64] & (1 << (usage % 64)) != 0 -} - -fn modifiers_of(held: &[u64; 4]) -> u8 { - let m = |a: u8, b: u8| is_held(held, a) || is_held(held, b); - (if m(0xE1, 0xE5) { MOD_SHIFT } else { 0 }) - | (if m(0xE0, 0xE4) { MOD_CTRL } else { 0 }) - | (if m(0xE2, 0xE6) { MOD_ALT } else { 0 }) - | (if m(0xE3, 0xE7) { MOD_GUI } else { 0 }) -} - -/// The held-modifier bitmask; test-only, no shipping caller. -#[cfg(feature = "boot-actuators")] -pub fn modifiers() -> u8 { - modifiers_of(&HELD.lock()) -} - -/// Queue one key transition; a transition to an already-held state queues nothing. -pub fn handle_key(usage: u8, pressed: bool) -> bool { - if usage == 0 { - return false; - } - let modifiers = { - let mut held = HELD.lock(); - if is_held(&held, usage) == pressed { - return false; - } - let word = &mut held[usage as usize / 64]; - let bit = 1u64 << (usage % 64); - if pressed { *word |= bit } else { *word &= !bit } - modifiers_of(&held) - }; - - let mut buf = KEY_BUF.lock(); - if buf.len() >= MAX_QUEUED_EVENTS { - buf.pop_front(); - } - buf.push_back(RawKeyEvent { - keycode: usage, - modifiers: if pressed { modifiers } else { modifiers | MOD_RELEASED }, - }); - true -} - /// Throw away everything queued; called when the device changes hands so no claimant inherits another's keystrokes. pub fn discard_queued() { KEY_BUF.lock().clear(); } -/// Synthesise a release for every held usage; recovers a keyboard that reset without releasing its keys. -pub fn release_all() -> usize { - let held = *HELD.lock(); - let mut n = 0; - for usage in 0..=u8::MAX { - if is_held(&held, usage) && handle_key(usage, false) { - n += 1; - } - } - n -} - /// Queue the transitions one USB keyboard's report made, and return how many queued. pub fn apply(transitions: toyos_usbhid::keyboard::Transitions) -> usize { + let mut buf = KEY_BUF.lock(); let mut queued = 0; for key in transitions { - if handle_key(key.usage, key.pressed) { - queued += 1; + if buf.len() >= MAX_QUEUED_EVENTS { + buf.pop_front(); } + let modifiers = if key.pressed { 0 } else { MOD_RELEASED }; + buf.push_back(RawKeyEvent { keycode: key.usage, modifiers }); + queued += 1; } queued } diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 1c8980c4d79..215cdc76cc6 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -43,8 +43,6 @@ mod power; mod keyboard; mod mouse; #[cfg(feature = "boot-actuators")] -mod input_merge_test; -#[cfg(feature = "boot-actuators")] mod usb_gate; #[cfg(feature = "boot-actuators")] mod sched_gate; @@ -548,12 +546,6 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { boot_phase!("devices ready", t_devices); - // Before userland, so nothing else is reading the input queues. - #[cfg(feature = "boot-actuators")] - if actuator::test_input_merge() { - input_merge_test::run(); - } - report_log_destination(); let complete = cpu::counter(); boot_phase!("complete", 0); diff --git a/kernel/src/mouse.rs b/kernel/src/mouse.rs index 687152a85c8..1d6fa3eaeb0 100644 --- a/kernel/src/mouse.rs +++ b/kernel/src/mouse.rs @@ -1,47 +1,38 @@ +//! The kernel's mouse stream: each USB pointer's reports, queued as the +//! device gave them for the claim's holder. +//! +//! **One source to the owner of input, merged nowhere here.** Each record +//! names its pointer ([`PointerSource`]) and carries that pointer's own +//! buttons and its motion, relative or absolute; the owner ORs every +//! pointer's buttons and keeps the one position (`toyos_input::Pointer`). + use alloc::collections::VecDeque; -use core::sync::atomic::{AtomicU8, AtomicU16, AtomicU64, Ordering}; +use core::sync::atomic::{AtomicU64, Ordering}; use crate::sync::Lock; use crate::watch::Watch; pub use toyos_abi::input::MouseEvent; +use toyos_abi::input::{POINTER_ABSOLUTE, POINTER_RELATIVE}; static MOUSE_BUF: Lock> = Lock::new(VecDeque::new()); -/// [`crate::keyboard::source_exists`]'s twin: the aux port armed, or any xHCI controller bound. -static SOURCE_EXISTS: core::sync::atomic::AtomicBool = - core::sync::atomic::AtomicBool::new(false); - -pub fn declare_source() { - SOURCE_EXISTS.store(true, Ordering::Relaxed); -} - -pub fn source_exists() -> bool { - SOURCE_EXISTS.load(Ordering::Relaxed) -} - /// How many pointer updates the kernel holds for a reader that is not reading. pub const MAX_QUEUED_EVENTS: usize = 512; -static LAST_X: AtomicU16 = AtomicU16::new(0); -static LAST_Y: AtomicU16 = AtomicU16::new(0); /// What a pointer poll waits on; no read blocks on the mouse. pub static WATCH: Watch = Watch::new(); +/// The buttons a record carries: boot-mouse order's five, which are what the +/// owner merges. +const BUTTONS: u8 = 0x1F; + /// Which physical pointer a report came from, keyed by device and not by bus. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct PointerSource(u8); -/// One bit per entry of `BUTTONS`, permanently set for bit 0 (the i8042's aux port). -static IN_USE: [AtomicU64; 4] = [ - AtomicU64::new(1), - AtomicU64::new(0), - AtomicU64::new(0), - AtomicU64::new(0), -]; +/// One bit per source a [`PointerSource`] can name. +static IN_USE: [AtomicU64; 4] = [const { AtomicU64::new(0) }; 4]; impl PointerSource { - /// The i8042's aux port, of which a machine has at most one. - pub const PS2: Self = Self(0); - - /// The lowest free entry in the button table, or `None` when every entry is held; a caller that gets `None` must not bind the device, or the two devices' buttons flap. + /// The lowest free source, or `None` when every one is held; a caller that gets `None` must not bind the device, or two devices' buttons share one record. pub fn claim() -> Option { for (word, bits) in IN_USE.iter().enumerate() { let mut seen = bits.load(Ordering::Relaxed); @@ -61,89 +52,32 @@ impl PointerSource { None } - /// Which entry of the button table this source publishes into. + /// What the records of this source carry as theirs. pub fn id(self) -> u8 { self.0 } } -/// One byte per source a `PointerSource` can name. -static BUTTONS: [AtomicU8; 256] = [const { AtomicU8::new(0) }; 256]; - -/// OR'd across sources, so a zero-buttons report from one live pointer cannot clear a button another pointer holds. -fn merged_buttons() -> u8 { - BUTTONS.iter().fold(0, |acc, b| acc | b.load(Ordering::Relaxed)) -} - /// Where a report puts the pointer. #[derive(Clone, Copy, Debug)] pub enum Motion { - Relative { dx: i32, dy: i32 }, + Relative { dx: i16, dy: i16 }, + /// Each axis at most `toyos_abi::input::POINTER_ABSOLUTE_MAX`. Absolute { x: u16, y: u16 }, } -// Per-axis, not a single scalar: the compositor's space is square and screens are not, -// so one shared scalar would skew diagonal motion by the screen's aspect ratio. -const REL_SCALE: i32 = 64; - -static SCALE_X: AtomicU16 = AtomicU16::new(REL_SCALE as u16); -static SCALE_Y: AtomicU16 = AtomicU16::new(REL_SCALE as u16); - -/// The per-axis scale a screen of this size wants, chosen so `x * width == y * height`. -pub fn rel_scale_for(width: u32, height: u32) -> (u16, u16) { - let short = width.min(height) as i32; - ( - (REL_SCALE * short / width.max(1) as i32).max(1) as u16, - (REL_SCALE * short / height.max(1) as i32).max(1) as u16, - ) -} - -/// Publish the geometry the absolute space is being mapped onto. -pub fn set_screen(width: u32, height: u32) { - if width == 0 || height == 0 { - return; - } - let (x, y) = rel_scale_for(width, height); - SCALE_X.store(x, Ordering::Relaxed); - SCALE_Y.store(y, Ordering::Relaxed); - crate::log!("mouse: rel scale x={} y={} (screen {}x{})", x, y, width, height); -} - -/// The only path that merges buttons and accumulates motion; queues an update and returns true iff one was queued. -pub fn handle_motion( - source: PointerSource, - buttons: u8, - motion: Motion, - scroll: i8, -) -> bool { - let prev = BUTTONS[source.0 as usize].swap(buttons, Ordering::Relaxed); - let merged = merged_buttons(); - - let last_x = LAST_X.load(Ordering::Relaxed); - let last_y = LAST_Y.load(Ordering::Relaxed); - let (abs_x, abs_y) = match motion { - Motion::Absolute { x, y } => (x, y), - Motion::Relative { dx, dy } => ( - (last_x as i32 + dx * SCALE_X.load(Ordering::Relaxed) as i32).clamp(0, 32767) as u16, - (last_y as i32 + dy * SCALE_Y.load(Ordering::Relaxed) as i32).clamp(0, 32767) as u16, - ), +/// Queue one report of `source`'s. +pub fn report(source: PointerSource, buttons: u8, motion: Motion, scroll: i8) { + let (kind, x, y) = match motion { + Motion::Relative { dx, dy } => (POINTER_RELATIVE, dx, dy), + // At most `POINTER_ABSOLUTE_MAX`, which an `i16` holds. + Motion::Absolute { x, y } => (POINTER_ABSOLUTE, x as i16, y as i16), }; - - if abs_x == last_x && abs_y == last_y && scroll == 0 && buttons == prev { - return false; - } - LAST_X.store(abs_x, Ordering::Relaxed); - LAST_Y.store(abs_y, Ordering::Relaxed); - queue(MouseEvent { buttons: merged, scroll, abs_x, abs_y }); - true -} - -fn queue(event: MouseEvent) { let mut buf = MOUSE_BUF.lock(); if buf.len() >= MAX_QUEUED_EVENTS { buf.pop_front(); } - buf.push_back(event); + buf.push_back(MouseEvent { source: source.0, buttons: buttons & BUTTONS, scroll, kind, x, y }); } /// Throw away everything queued, for the reason [`crate::keyboard::discard_queued`] gives. @@ -151,34 +85,16 @@ pub fn discard_queued() { MOUSE_BUF.lock().clear(); } -/// Release everything a source was holding, and publish the result. A source that goes silent without this call leaves its button stuck down and the compositor stuck mid-drag. -pub fn release_buttons(source: PointerSource) -> bool { - let before = merged_buttons(); - BUTTONS[source.0 as usize].store(0, Ordering::Relaxed); - let after = merged_buttons(); - if before == after { - return false; - } - queue(MouseEvent { - buttons: after, - scroll: 0, - abs_x: LAST_X.load(Ordering::Relaxed), - abs_y: LAST_Y.load(Ordering::Relaxed), - }); - true +/// `source` holds no button now. A source that goes silent without this call leaves its button stuck down and the owner stuck mid-drag. +pub fn release_buttons(source: PointerSource) { + report(source, 0, Motion::Relative { dx: 0, dy: 0 }, 0); } -/// A pointer that is gone: release whatever it was holding and give its entry back. Returns whether the release changed the merge. -pub fn unbind(source: PointerSource) -> bool { - assert!( - source != PointerSource::PS2, - "mouse: the i8042's aux port cannot be unplugged, and freeing entry 0 would let a USB \ - pointer alias it" - ); - let published = release_buttons(source); - // Buttons must clear before the entry frees, or the next device inherits a stale bit. +/// A pointer that is gone: release whatever it was holding and give its source back. +pub fn unbind(source: PointerSource) { + release_buttons(source); + // Released before the source frees, or the next device's records follow a stale button. IN_USE[source.0 as usize / 64].fetch_and(!(1u64 << (source.0 % 64)), Ordering::Relaxed); - published } pub fn has_data() -> bool { diff --git a/kernel/src/sched/driver.rs b/kernel/src/sched/driver.rs index 6ebf928f0d1..8b1841aa5b6 100644 --- a/kernel/src/sched/driver.rs +++ b/kernel/src/sched/driver.rs @@ -642,8 +642,6 @@ fn execute(action: Action) { let awake = cpus().get(cpu).doorbell().kick_pending() || crate::preempt::need_resched() || !with_cpu(|c| c.mailbox_is_empty()) - // The i8042 verdict needs a pass to notice its deadline; a quiet machine after boot runs none otherwise. - || crate::arch::keyboard_controller::verdict_due() // No log condition here: a log to write means a runnable process, covered above. A pending // root-hub port needs a pass too — no interrupt is coming. || crate::drivers::xhci::port_work_pending(); @@ -660,7 +658,6 @@ fn execute(action: Action) { /// Consume this CPU's `irq_ring` records into wakes, before the mailbox drain, so a wake posted here reaches this pass's pick. fn drain_irqs() { crate::drivers::xhci::poll_if_pending(); - crate::arch::keyboard_controller::service(); } /// Leave the current stack for this CPU's idle stack and never come back. diff --git a/kernel/usbhid/Cargo.toml b/kernel/usbhid/Cargo.toml index b2518ebc5e0..b586e5b5deb 100644 --- a/kernel/usbhid/Cargo.toml +++ b/kernel/usbhid/Cargo.toml @@ -7,3 +7,7 @@ description = "USB HID boot keyboard and pointer reports in, key transitions and version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" + +# One keyboard's held set, which the owner of input holds per source too. +[dependencies] +toyos-input = { path = "../../lib/input" } diff --git a/kernel/usbhid/src/keyboard.rs b/kernel/usbhid/src/keyboard.rs index 67e00fd6078..8bedf7c993d 100644 --- a/kernel/usbhid/src/keyboard.rs +++ b/kernel/usbhid/src/keyboard.rs @@ -28,6 +28,9 @@ //! `usbkbd` reads every slot byte above 3 through a 256-entry table. What a //! usage means is the translator's to say. +pub use toyos_input::keys::{Transition, Transitions}; +use toyos_input::keys::{Keys, Usages}; + /// Bytes in a boot keyboard report, and what its interrupt transfer asks for. pub const REPORT: usize = 8; @@ -35,16 +38,6 @@ const ERROR_ROLL_OVER: u8 = 0x01; const POST_FAIL: u8 = 0x02; const ERROR_UNDEFINED: u8 = 0x03; -/// The first modifier's usage; byte 0's bit n is `MODIFIER + n`. -const MODIFIER: u8 = 0xE0; - -/// One key went down or up, named by its Keyboard/Keypad page usage. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Transition { - pub usage: u8, - pub pressed: bool, -} - /// Why a report was not believed at all; the keyboard's state is the last /// report that was. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -57,16 +50,6 @@ pub enum Refused { Undefined, } -/// Every usage, one bit each. -type Usages = [u64; 4]; - -fn bit(usage: u8) -> (usize, u64) { - (usize::from(usage / 64), 1 << (usage % 64)) -} - -/// The eight modifiers' bits, which sit in the last word. -const MODIFIERS: Usages = [0, 0, 0, 0xFF << (MODIFIER % 64)]; - /// What a believed report did. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Read { @@ -84,7 +67,7 @@ pub enum Read { #[derive(PartialEq, Eq, Debug, Default)] #[cfg_attr(test, derive(Clone))] pub struct Keyboard { - held: Usages, + keys: Keys, } impl Keyboard { @@ -92,8 +75,7 @@ impl Keyboard { /// is not believed. pub fn report(&mut self, report: &[u8]) -> Result { let report: &[u8; REPORT] = report.try_into().map_err(|_| Refused::Length(report.len()))?; - // Byte 0's bit n is usage `MODIFIER + n`, which is bit `MODIFIER % 64 + n` of the last word. - let modifiers: Usages = [0, 0, 0, u64::from(report[0]) << (MODIFIER % 64)]; + let modifiers = Usages::modifier_byte(report[0]); let mut now = modifiers; let mut rolled_over = false; for &usage in &report[2..] { @@ -102,75 +84,19 @@ impl Keyboard { ERROR_ROLL_OVER => rolled_over = true, POST_FAIL => return Err(Refused::PostFail), ERROR_UNDEFINED => return Err(Refused::Undefined), - usage => { - let (word, mask) = bit(usage); - now[word] |= mask; - } + usage => now.insert(usage), } } if rolled_over { - let kept = core::array::from_fn(|w| self.held[w] & !MODIFIERS[w] | modifiers[w]); - return Ok(Read::RollOver(self.hold(kept))); + let kept = self.keys.held().keys().union(modifiers); + return Ok(Read::RollOver(self.keys.hold(kept))); } - Ok(Read::Keys(self.hold(now))) + Ok(Read::Keys(self.keys.hold(now))) } /// Every held key released: a keyboard leaving the bus says nothing more. pub fn release(&mut self) -> Transitions { - self.hold([0; 4]) - } - - fn hold(&mut self, now: Usages) -> Transitions { - let changed = core::array::from_fn(|w| self.held[w] ^ now[w]); - self.held = now; - Transitions { changed, now, pass: Pass::Modifiers } - } -} - -/// Which transitions [`Transitions`] is yielding. **Modifiers first**, so a -/// report pressing Shift and a key together is a shifted key and one -/// pressing Ctrl, Alt and D together is the chord; then releases, then -/// presses. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum Pass { - Modifiers, - Released, - Pressed, -} - -/// The transitions one report made, each usage at most once and in ascending -/// usage within each pass. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Transitions { - changed: Usages, - now: Usages, - pass: Pass, -} - -impl Iterator for Transitions { - type Item = Transition; - - fn next(&mut self) -> Option { - loop { - // The modifiers' pass has cleared their bits before the others run. - let wanted = |w: usize| match self.pass { - Pass::Modifiers => self.changed[w] & MODIFIERS[w], - Pass::Released => self.changed[w] & !self.now[w], - Pass::Pressed => self.changed[w] & self.now[w], - }; - if let Some(w) = (0..4).find(|&w| wanted(w) != 0) { - let at = wanted(w).trailing_zeros(); - self.changed[w] &= !(1 << at); - // `w < 4` and `at < 64`, so the usage is a byte. - let usage = (w as u32 * 64 + at) as u8; - return Some(Transition { usage, pressed: self.now[w] & (1 << at) != 0 }); - } - self.pass = match self.pass { - Pass::Modifiers => Pass::Released, - Pass::Released => Pass::Pressed, - Pass::Pressed => return None, - }; - } + self.keys.hold(Usages::NONE) } } diff --git a/lib/acpi/src/fadt.rs b/lib/acpi/src/fadt.rs index e1fc086ec90..049dba9b28e 100644 --- a/lib/acpi/src/fadt.rs +++ b/lib/acpi/src/fadt.rs @@ -9,7 +9,6 @@ pub(crate) const FADT_FIRMWARE_CTRL: usize = 36; pub const FADT_DSDT: usize = 40; pub const FADT_PM1A_CNT_BLK: usize = 64; const FADT_CENTURY: usize = 108; -const FADT_IAPC_BOOT_ARCH: usize = 109; const FADT_FLAGS: usize = 112; /// A Generic Address Structure (§5.2.3.2): space at +0, bit width at +1, bit offset at +2, address at +4. const FADT_RESET_REG: usize = 116; @@ -29,17 +28,6 @@ const SPACE_SYSTEM_MEMORY: u8 = 0; const SPACE_SYSTEM_IO: u8 = 1; const SPACE_PCI_CONFIG: u8 = 2; -// `Err` is not "absent" and must not be treated as one by the caller. -// Bit 1 of the flags is the port 60/64 keyboard-controller bit, defined only from FADT revision 3 onward. -pub fn iapc_boot_arch(phys: P, rsdp_addr: u64) -> Result<(u8, u16), TableError> { - const NEEDED: usize = FADT_IAPC_BOOT_ARCH + 2; - let fadt = find_table(phys, rsdp_addr, b"FACP", NEEDED)?; - let short = || TableError::Length { declared: fadt.len() as u32, needed: NEEDED }; - let revision = fadt.byte(SDT_REVISION).ok_or_else(short)?; - let flags = fadt.u16_at(FADT_IAPC_BOOT_ARCH).ok_or_else(short)?; - Ok((revision, flags)) -} - /// What the FADT says about the RTC's century register. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Century { diff --git a/lib/acpi/src/lib.rs b/lib/acpi/src/lib.rs index 5941cb22b74..626f7d48bc9 100644 --- a/lib/acpi/src/lib.rs +++ b/lib/acpi/src/lib.rs @@ -31,7 +31,7 @@ use toyos_bootmap::DirectMapEnd; pub use ecdt::{ecdt, Ec, EcRefused, EcRegister, ECDT_NEEDED}; pub use facs::{acquire, facs, release, Facs, FacsRefused, FACS_GLOBAL_LOCK, OWNED, PENDING}; pub use fadt::{ - century_of, dsdt_address, fixed_hardware, iapc_boot_arch, pm1a_control, psci, reset_register, + century_of, dsdt_address, fixed_hardware, pm1a_control, psci, reset_register, rtc_century, Century, Field, FixedHardware, FixedRefused, LegacyMode, PowerButton, Psci, Reset, SmiCmd, CMOS_RAM, FADT_FOR_FIXED_HARDWARE, FADT_PM1A_CNT_BLK, diff --git a/lib/acpi/tests/corpus.rs b/lib/acpi/tests/corpus.rs index 807a7019528..64ea5b48ec9 100644 --- a/lib/acpi/tests/corpus.rs +++ b/lib/acpi/tests/corpus.rs @@ -12,7 +12,7 @@ use common::{declare_len, entry, madt, rsdp, sdt, t14_root_bridge, xsdt, Machine use toyos_abi::boot::RootBridgeWindow; use toyos_abi::acpi::Block; use toyos_acpi::{ - definition_blocks, dsdt_address, ecam_allocations, find_by_id, find_table, fixed_hardware, hpet_base, iapc_boot_arch, isa_line, + definition_blocks, dsdt_address, ecam_allocations, find_by_id, find_table, fixed_hardware, hpet_base, isa_line, madt_entries, memory_windows, pm1a_control, psci, reset_register, rtc_century, sci_line, Century, Field, FixedRefused, LegacyMode, Line, MadtEntry, MadtHalt, Phys, Polarity, PowerButton, Psci, SmiCmd, Reset, SourceOverride, Table, TableError, Trigger, @@ -254,27 +254,18 @@ fn a_fadt_of_the_acpi_1_0_length_is_read_without_its_x_fields() { let fadt = find_table(m, RSDP_AT, b"FACP", 109).expect("FADT"); assert_eq!(dsdt_address(&fadt), 0x7ffb_9000); assert_eq!(rtc_century(m, RSDP_AT), Ok(Century::At(0x32))); - // The flags word at 109 is inside 116 bytes, so it is *read* — and it comes - // back with revision 1 beside it, which is the whole of what tells the - // caller that bit 1 means nothing on this firmware. - assert_eq!(iapc_boot_arch(m, RSDP_AT), Ok((1, 0))); } -/// A FADT that stops before the flags word: a refusal, never a zero. +/// A FADT that ends with the century byte reads it. #[test] -fn a_fadt_too_short_for_the_boot_architecture_flags_is_refused() { +fn a_fadt_that_ends_at_the_century_byte_reads_it() { let t = sdt(b"FACP", 1, &[0u8; 73]); assert_eq!(t.len(), 109); let head = rsdp(XSDT_AT, 2, 36); let root = xsdt(&[TABLE_AT]); let regions: &[(u64, &[u8])] = &[(RSDP_AT, &head), (XSDT_AT, &root), (TABLE_AT, &t)]; let m = Machine { regions }; - assert_eq!( - iapc_boot_arch(m, RSDP_AT).err(), - Some(TableError::Length { declared: 109, needed: 111 }), - "a FADT too short for the flags is not a FADT the i8042 driver may read" - ); - // The century byte at 108 is the last one it does hold. + // The century byte at 108 is the last one it holds. assert_eq!(rtc_century(m, RSDP_AT), Ok(Century::Absent)); } @@ -351,7 +342,7 @@ const FIXTURES: &[(&str, &[u8], u64)] = &[ /// **No panic and no unbounded walk, over every byte of every table this crate /// decodes.** Each byte of each fixture takes each of its 255 other values in -/// turn, and the MCFG walk, `hpet_base`, `rtc_century`, `iapc_boot_arch` and the +/// turn, and the MCFG walk, `hpet_base`, `rtc_century` and the /// MADT walk to exhaustion all run over it. A panic anywhere — including the /// reader's own, which fires on a read no bound accepted — reds this. /// @@ -398,7 +389,6 @@ fn no_single_byte_mutation_of_a_real_table_panics_or_runs_away() { } let _ = hpet_base(m, rsdp_at); let _ = rtc_century(m, rsdp_at); - let _ = iapc_boot_arch(m, rsdp_at); if let Ok(blocks) = definition_blocks(m, rsdp_at) { // The DSDT, and at most an item an 8-byte entry of the largest XSDT. assert!(blocks.count() <= 1 + MAX_TABLE_LEN / 8, "{which}[{offset}]={value:#04x}: the block walk is not ending"); diff --git a/lib/acpi/tests/fixtures.rs b/lib/acpi/tests/fixtures.rs index d9683902b85..7626d970fbe 100644 --- a/lib/acpi/tests/fixtures.rs +++ b/lib/acpi/tests/fixtures.rs @@ -7,7 +7,7 @@ use common::{t14_root_bridge, Machine, OVMF_ROOT_BRIDGE}; use toyos_abi::boot::RootBridgeWindow; use toyos_abi::acpi::Block; use toyos_acpi::{ - century_of, definition_blocks, dsdt_address, ecam_allocations, find_table, fixed_hardware, hpet_base, iapc_boot_arch, + century_of, definition_blocks, dsdt_address, ecam_allocations, find_table, fixed_hardware, hpet_base, isa_line, madt_entries, memory_windows, pm1a_control, psci, reset_register, rtc_century, sci_line, Century, FixedHardware, IoApicEntry, Line, MadtEntry, Polarity, PowerButton, Psci, Reset, SmiCmd, SourceOverride, TableError, Trigger, FADT_FOR_FIXED_HARDWARE, FADT_PM1A_CNT_BLK, @@ -151,12 +151,6 @@ fn the_q35_fadt_predates_arm_boot_arch() { assert_eq!(psci(&fadt), Psci::Undefined { revision: 3, minor: 0 }); } -#[test] -fn the_boot_architecture_flags_come_off_a_revision_that_defines_them() { - let (revision, flags) = iapc_boot_arch(machine(), RSDP).expect("FADT"); - assert_eq!((revision, flags), (3, 0x2)); -} - #[test] fn the_xsdt_walk_reaches_every_entry() { let m = machine(); diff --git a/lib/elf/Cargo.toml b/lib/elf/Cargo.toml index 86833e266d8..4ea9f3f706b 100644 --- a/lib/elf/Cargo.toml +++ b/lib/elf/Cargo.toml @@ -1,5 +1,5 @@ -# A member of the host workspace (root `Cargo.toml`), like toyos-gpt and -# toyos-ps2: the kernel depends on it by path and its tests run on the host, +# A member of the host workspace (root `Cargo.toml`), like toyos-gpt: the +# kernel depends on it by path and its tests run on the host, # with no target-triple juggling. An ELF is bytes somebody else wrote, and every # one the kernel opens crossed a trust boundary — so the parser has to be # exercised against hostile bytes far more often than a QEMU boot allows. diff --git a/lib/fat32/Cargo.toml b/lib/fat32/Cargo.toml index bd76ef071a8..8653ba59fe4 100644 --- a/lib/fat32/Cargo.toml +++ b/lib/fat32/Cargo.toml @@ -1,4 +1,4 @@ -# A member of the host workspace (root `Cargo.toml`), like toyos-ps2: the +# A member of the host workspace (root `Cargo.toml`): the # kernel depends on it by path and its tests run on the host, with no # target-triple juggling. Membership adds no dependency edge — a # workspace shares a lockfile and nothing else. diff --git a/lib/gpt/Cargo.toml b/lib/gpt/Cargo.toml index 065fa71fd4b..45ff37c3c48 100644 --- a/lib/gpt/Cargo.toml +++ b/lib/gpt/Cargo.toml @@ -1,4 +1,4 @@ -# A member of the host workspace (root `Cargo.toml`), like toyos-ps2: the +# A member of the host workspace (root `Cargo.toml`): the # kernel depends on it by path and its tests run on the host, with no # target-triple juggling. A GPT is bytes somebody else wrote, so the # parser has to be exercised against hostile bytes far more often than a QEMU diff --git a/lib/hda/Cargo.toml b/lib/hda/Cargo.toml index 5ed67655fd6..945ca2b8774 100644 --- a/lib/hda/Cargo.toml +++ b/lib/hda/Cargo.toml @@ -1,5 +1,5 @@ -# A member of the host workspace (root `Cargo.toml`), like toyos-gpt and -# toyos-ps2: its tests run on the host with no target-triple juggling, and +# A member of the host workspace (root `Cargo.toml`), like toyos-gpt: its +# tests run on the host with no target-triple juggling, and # soundserver depends on it by path. A codec's answers are bytes a device wrote, so # the decoders have to be exercised against hostile ones far more often than a # QEMU boot allows — and the one machine that matters cannot be booted by a test diff --git a/lib/input/Cargo.toml b/lib/input/Cargo.toml new file mode 100644 index 00000000000..2fb9233a6f2 --- /dev/null +++ b/lib/input/Cargo.toml @@ -0,0 +1,13 @@ +# A member of the host workspace (root `Cargo.toml`): the kernel links its +# `Keys` through toyos-usbhid, the owners of input link the rest, and its tests +# run on the host. + +[package] +name = "toyos-input" +description = "The owner of input's side of every source: one keyboard's held keys, the merge of every source's keys and pointers, and the `input` port's records, each one that does not decode refused by name; pure." +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" + +[dependencies] +toyos-abi = { path = "../../sdk/abi" } diff --git a/lib/input/src/keys.rs b/lib/input/src/keys.rs new file mode 100644 index 00000000000..b6fc783e343 --- /dev/null +++ b/lib/input/src/keys.rs @@ -0,0 +1,134 @@ +//! One keyboard's held keys, and the transitions between two sets of them. +//! +//! A transition is the difference between two sets of **one** keyboard's +//! keys: diffing against another's would release keys still down. A usage +//! held twice is held once. + +/// The first modifier's usage: LeftControl, then LeftShift, LeftAlt, +/// LeftGUI and their right-hand four (HID Usage Tables, Keyboard/Keypad page). +pub const MODIFIER: u8 = 0xE0; + +/// A set of Keyboard/Keypad page usages, one bit each. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +pub struct Usages([u64; 4]); + +fn bit(usage: u8) -> (usize, u64) { + (usize::from(usage / 64), 1 << (usage % 64)) +} + +/// The eight modifiers' bits, which sit in the last word. +const MODIFIERS: Usages = Usages([0, 0, 0, 0xFF << (MODIFIER % 64)]); + +impl Usages { + pub const NONE: Self = Self([0; 4]); + + /// The modifiers a boot keyboard report's byte 0 holds: its bit n is + /// usage `MODIFIER + n`. + pub const fn modifier_byte(byte: u8) -> Self { + Self([0, 0, 0, (byte as u64) << (MODIFIER % 64)]) + } + + pub fn contains(self, usage: u8) -> bool { + let (word, mask) = bit(usage); + self.0[word] & mask != 0 + } + + pub fn insert(&mut self, usage: u8) { + let (word, mask) = bit(usage); + self.0[word] |= mask; + } + + pub fn remove(&mut self, usage: u8) { + let (word, mask) = bit(usage); + self.0[word] &= !mask; + } + + pub fn union(self, other: Self) -> Self { + Self(core::array::from_fn(|w| self.0[w] | other.0[w])) + } + + /// The modifiers among these. + pub fn modifiers(self) -> Self { + Self(core::array::from_fn(|w| self.0[w] & MODIFIERS.0[w])) + } + + /// Every one of these but the modifiers. + pub fn keys(self) -> Self { + Self(core::array::from_fn(|w| self.0[w] & !MODIFIERS.0[w])) + } +} + +/// One key went down or up, named by its Keyboard/Keypad page usage. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Transition { + pub usage: u8, + pub pressed: bool, +} + +/// One keyboard's keys held, and by `Default` none. +/// +/// Not `Copy`: a copy would hold keys the keyboard itself goes on to release. +#[derive(Clone, PartialEq, Eq, Debug, Default)] +pub struct Keys { + held: Usages, +} + +impl Keys { + /// What is held. + pub fn held(&self) -> Usages { + self.held + } + + /// Hold `now` and nothing else, and answer the transitions that made it. + pub fn hold(&mut self, now: Usages) -> Transitions { + let changed = Usages(core::array::from_fn(|w| self.held.0[w] ^ now.0[w])); + self.held = now; + Transitions { changed, now, pass: Pass::Modifiers } + } +} + +/// Which transitions [`Transitions`] is yielding. **Modifiers first**, so a +/// set pressing Shift and a key together is a shifted key and one pressing +/// Ctrl, Alt and D together is the chord; then releases, then presses. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Pass { + Modifiers, + Released, + Pressed, +} + +/// The transitions one change of set made, each usage at most once and in +/// ascending usage within each pass. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Transitions { + changed: Usages, + now: Usages, + pass: Pass, +} + +impl Iterator for Transitions { + type Item = Transition; + + fn next(&mut self) -> Option { + loop { + // The modifiers' pass has cleared their bits before the others run. + let wanted = |w: usize| match self.pass { + Pass::Modifiers => self.changed.0[w] & MODIFIERS.0[w], + Pass::Released => self.changed.0[w] & !self.now.0[w], + Pass::Pressed => self.changed.0[w] & self.now.0[w], + }; + if let Some(w) = (0..4).find(|&w| wanted(w) != 0) { + let at = wanted(w).trailing_zeros(); + self.changed.0[w] &= !(1 << at); + // `w < 4` and `at < 64`, so the usage is a byte. + let usage = (w as u32 * 64 + at) as u8; + return Some(Transition { usage, pressed: self.now.0[w] & (1 << at) != 0 }); + } + self.pass = match self.pass { + Pass::Modifiers => Pass::Released, + Pass::Released => Pass::Pressed, + Pass::Pressed => return None, + }; + } + } +} diff --git a/lib/input/src/lib.rs b/lib/input/src/lib.rs new file mode 100644 index 00000000000..d7186080186 --- /dev/null +++ b/lib/input/src/lib.rs @@ -0,0 +1,23 @@ +//! Input, as its owner sees it: every source's keys and pointers, merged. +//! +//! **A source says what one device did and nothing more**: a key's +//! transition with no modifier mask, a pointer's own buttons and its motion +//! as the device gave it. The owner of input keeps a [`Merge`] and a +//! [`Pointer`] over every source it holds, so a key or a button is held while +//! any source holds it, and a source that ends releases what it held and +//! nothing another holds. +//! +//! [`wire`] is the one reader of a source's records, whether they came off the +//! `input` port or the kernel's streams: what does not decode is refused by +//! name. [`keys`] is one keyboard's held set, which the kernel's USB keyboard +//! decode holds too. Nothing here touches a device, allocates or holds a lock. + +#![no_std] +#![forbid(unsafe_code)] + +pub mod keys; +pub mod merge; +pub mod wire; + +pub use keys::Keys; +pub use merge::{rel_scale_for, Merge, Pointer, Slot, State}; diff --git a/lib/input/src/merge.rs b/lib/input/src/merge.rs new file mode 100644 index 00000000000..6fe95a85d95 --- /dev/null +++ b/lib/input/src/merge.rs @@ -0,0 +1,166 @@ +//! The owner of input's merge: every source's keys into one keyboard, and +//! every pointer's buttons and motion into one pointer. + +use toyos_abi::input::{RawKeyEvent, MOD_ALT, MOD_CTRL, MOD_GUI, MOD_RELEASED, MOD_SHIFT, POINTER_ABSOLUTE_MAX}; + +use crate::keys::{Keys, Transitions, Usages}; +use crate::wire::{Motion, Report, CONNECTIONS}; + +/// The sources the owner merges: the kernel's streams, until usbd takes the +/// last kernel source, and one per connection on the port. +pub const SLOTS: usize = CONNECTIONS + 1; + +/// Which source a record came from. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Slot(usize); + +impl Slot { + /// The kernel's keyboard and mouse streams. + pub const KERNEL: Self = Self(0); + + /// The `n`th connection on the port, of [`CONNECTIONS`]. + pub fn connection(n: usize) -> Option { + (n < CONNECTIONS).then_some(Self(n + 1)) + } +} + +/// One keyboard made of every source's: a key is held while any source holds +/// it. +#[derive(Default)] +pub struct Merge { + sources: [Keys; SLOTS], + union: Keys, +} + +impl Merge { + /// `key` from `slot`: what it changed of the union, each transition with + /// the union's modifiers after it. A repeated make, or a key another + /// source already holds, changes nothing. + pub fn key(&mut self, slot: Slot, key: RawKeyEvent) -> Delivered { + let mut now = self.sources[slot.0].held(); + if key.pressed() { + now.insert(key.keycode); + } else { + now.remove(key.keycode); + } + self.sources[slot.0].hold(now); + self.merge() + } + + /// `slot` holds nothing: its source ended, or lost what it held. + pub fn end(&mut self, slot: Slot) -> Delivered { + self.sources[slot.0].hold(Usages::NONE); + self.merge() + } + + fn merge(&mut self) -> Delivered { + let union = self.sources.iter().fold(Usages::NONE, |all, keys| all.union(keys.held())); + let before = self.union.held(); + Delivered { transitions: self.union.hold(union), held: before } + } +} + +/// The transitions one record made of the union, in [`Transitions`]' order. +pub struct Delivered { + transitions: Transitions, + /// The union as of the transition last yielded. + held: Usages, +} + +impl Iterator for Delivered { + type Item = RawKeyEvent; + + fn next(&mut self) -> Option { + let made = self.transitions.next()?; + if made.pressed { + self.held.insert(made.usage); + } else { + self.held.remove(made.usage); + } + let released = if made.pressed { 0 } else { MOD_RELEASED }; + Some(RawKeyEvent { keycode: made.usage, modifiers: modifiers(self.held) | released }) + } +} + +fn modifiers(held: Usages) -> u8 { + let either = |left: u8, right: u8| held.contains(left) || held.contains(right); + (if either(0xE1, 0xE5) { MOD_SHIFT } else { 0 }) + | (if either(0xE0, 0xE4) { MOD_CTRL } else { 0 }) + | (if either(0xE2, 0xE6) { MOD_ALT } else { 0 }) + | (if either(0xE3, 0xE7) { MOD_GUI } else { 0 }) +} + +/// Counts per screen pixel's worth of the absolute space, before the +/// per-axis scale. +const REL_SCALE: i32 = 64; + +/// The per-axis scale a screen of this size wants, chosen so a count moves +/// as far across as down: `x * width == y * height`. Per-axis, not a single +/// scalar: the absolute space is square and screens are not, so one shared +/// scalar would skew diagonal motion by the screen's aspect ratio. +pub fn rel_scale_for(width: u32, height: u32) -> (u16, u16) { + let short = width.min(height) as i32; + ( + (REL_SCALE * short / width.max(1) as i32).max(1) as u16, + (REL_SCALE * short / height.max(1) as i32).max(1) as u16, + ) +} + +/// The one pointer: each pointer's buttons ORed, and one position in the +/// absolute space, `0..=POINTER_ABSOLUTE_MAX` on each axis. +pub struct Pointer { + /// Each pointer's buttons, by its slot and its record's `source`. + buttons: [[u8; 256]; SLOTS], + x: i32, + y: i32, + scale: (u16, u16), +} + +/// What the one pointer is after a record. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +pub struct State { + pub buttons: u8, + pub scroll: i8, + pub x: u16, + pub y: u16, +} + +impl Pointer { + /// At the middle of a `width` by `height` screen, holding nothing. + pub fn new(width: u32, height: u32) -> Self { + let middle = (POINTER_ABSOLUTE_MAX as i32 + 1) / 2; + Self { buttons: [[0; 256]; SLOTS], x: middle, y: middle, scale: rel_scale_for(width, height) } + } + + /// The screen relative motion is scaled for. + pub fn screen(&mut self, width: u32, height: u32) { + self.scale = rel_scale_for(width, height); + } + + /// `report` from `slot`: relative motion moves the position by the + /// screen's scale, and absolute motion sets it. + pub fn report(&mut self, slot: Slot, report: Report) -> State { + self.buttons[slot.0][usize::from(report.source)] = report.buttons; + let most = i32::from(POINTER_ABSOLUTE_MAX); + match report.motion { + Motion::Relative { dx, dy } => { + self.x = (self.x + i32::from(dx) * i32::from(self.scale.0)).clamp(0, most); + self.y = (self.y + i32::from(dy) * i32::from(self.scale.1)).clamp(0, most); + } + Motion::Absolute { x, y } => (self.x, self.y) = (i32::from(x), i32::from(y)), + } + self.state(report.scroll) + } + + /// Every pointer `slot` carried holds nothing. + pub fn end(&mut self, slot: Slot) -> State { + self.buttons[slot.0] = [0; 256]; + self.state(0) + } + + fn state(&self, scroll: i8) -> State { + let buttons = self.buttons.iter().flatten().fold(0, |all, &held| all | held); + // Both are clamped to `0..=POINTER_ABSOLUTE_MAX`. + State { buttons, scroll, x: self.x as u16, y: self.y as u16 } + } +} diff --git a/lib/input/src/wire.rs b/lib/input/src/wire.rs new file mode 100644 index 00000000000..ea9282de557 --- /dev/null +++ b/lib/input/src/wire.rs @@ -0,0 +1,155 @@ +//! The `input` port's frames, and the records the kernel's streams carry, +//! decoded by one reader whichever source sent them. +//! +//! Every byte here is one a source chose, and a source is outside the owner's +//! trust: a record that does not decode is refused by name and changes +//! nothing. + +use toyos_abi::input::{MouseEvent, RawKeyEvent, MOD_RELEASED, POINTER_ABSOLUTE, POINTER_ABSOLUTE_MAX, POINTER_RELATIVE}; + +/// The port the owner of input serves. +pub const PORT: &str = "input"; + +/// The one row that may receive [`PORT`]: a connector is the authority to type +/// into the focused window. +pub const HOLDER: &str = "ps2server"; + +/// The most connections the owner serves at once; one past it is refused. +pub const CONNECTIONS: usize = 4; + +/// A key: one [`RawKeyEvent`]. +pub const MSG_KEY: u32 = 1; +/// A pointer report: one [`MouseEvent`]. +pub const MSG_POINTER: u32 = 2; +/// No payload: release everything this source holds. +pub const MSG_LOST: u32 = 3; + +/// A key's bytes on the wire. +pub const KEY_LEN: usize = core::mem::size_of::(); +/// A pointer report's bytes on the wire. +pub const POINTER_LEN: usize = core::mem::size_of::(); +/// How much of a frame's payload the owner keeps: one byte past the longest +/// record, so a longer frame reads as one and is refused. +pub const KEEP: usize = POINTER_LEN + 1; + +/// The buttons boot-mouse order names. +const BUTTONS: u8 = 0x1F; + +/// Where a pointer report puts the pointer. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Motion { + /// A movement in the device's counts, down the screen positive. + Relative { dx: i16, dy: i16 }, + /// A position, each axis in `0..=POINTER_ABSOLUTE_MAX`. + Absolute { x: u16, y: u16 }, +} + +/// One pointer report, believed. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Report { + /// Which of its sender's pointers this is. + pub source: u8, + /// That pointer's own buttons, in boot-mouse order. + pub buttons: u8, + pub scroll: i8, + pub motion: Motion, +} + +/// One thing a source said. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Record { + Key(RawKeyEvent), + Pointer(Report), + Lost, +} + +/// Why a record was not believed. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Refused { + /// A frame type this port does not carry. + Type(u32), + /// A payload of the wrong length for its type. + Length { msg_type: u32, len: usize }, + /// Usage 0, which names no key. + NoUsage, + /// A modifier bit beside `MOD_RELEASED`: the mask is the owner's to make. + Modifiers(u8), + /// Neither relative nor absolute. + Kind(u8), + /// A button past boot-mouse order's five. + Buttons(u8), + /// An absolute position off the surface. + Position { x: i16, y: i16 }, +} + +impl Report { + /// This report as its wire record. + pub fn event(&self) -> MouseEvent { + let (kind, x, y) = match self.motion { + Motion::Relative { dx, dy } => (POINTER_RELATIVE, dx, dy), + // Both are at most `POINTER_ABSOLUTE_MAX`, which an `i16` holds. + Motion::Absolute { x, y } => (POINTER_ABSOLUTE, x as i16, y as i16), + }; + MouseEvent { source: self.source, buttons: self.buttons, scroll: self.scroll, kind, x, y } + } +} + +impl Record { + /// The frame this record is: its type, and its payload in `out`'s first + /// bytes, whose count is the third word. + pub fn frame(&self, out: &mut [u8; POINTER_LEN]) -> (u32, usize) { + let (msg_type, bytes): (u32, &[u8]) = match self { + Self::Key(key) => (MSG_KEY, toyos_abi::usersafe::bytes(key)), + Self::Pointer(report) => { + let event = report.event(); + out.copy_from_slice(toyos_abi::usersafe::bytes(&event)); + return (MSG_POINTER, POINTER_LEN); + } + Self::Lost => (MSG_LOST, &[]), + }; + out[..bytes.len()].copy_from_slice(bytes); + (msg_type, bytes.len()) + } +} + +/// The record a frame of `msg_type` carrying `payload` is. +pub fn decode(msg_type: u32, payload: &[u8]) -> Result { + let length = || Refused::Length { msg_type, len: payload.len() }; + match msg_type { + MSG_KEY => key(payload.try_into().map_err(|_| length())?).map(Record::Key), + MSG_POINTER => pointer(payload.try_into().map_err(|_| length())?).map(Record::Pointer), + MSG_LOST if payload.is_empty() => Ok(Record::Lost), + MSG_LOST => Err(length()), + other => Err(Refused::Type(other)), + } +} + +/// One key, as a source's frame or the kernel's keyboard stream carries it. +pub fn key(bytes: &[u8; KEY_LEN]) -> Result { + let [keycode, modifiers] = *bytes; + if keycode == 0 { + return Err(Refused::NoUsage); + } + if modifiers & !MOD_RELEASED != 0 { + return Err(Refused::Modifiers(modifiers)); + } + Ok(RawKeyEvent { keycode, modifiers }) +} + +/// One pointer report, as a source's frame or the kernel's mouse stream +/// carries it. +pub fn pointer(bytes: &[u8; POINTER_LEN]) -> Result { + let [source, buttons, scroll, kind, x0, x1, y0, y1] = *bytes; + let (x, y) = (i16::from_ne_bytes([x0, x1]), i16::from_ne_bytes([y0, y1])); + if buttons & !BUTTONS != 0 { + return Err(Refused::Buttons(buttons)); + } + let on_surface = |v: i16| (0..=POINTER_ABSOLUTE_MAX).contains(&v); + let motion = match kind { + POINTER_RELATIVE => Motion::Relative { dx: x, dy: y }, + POINTER_ABSOLUTE if on_surface(x) && on_surface(y) => Motion::Absolute { x: x as u16, y: y as u16 }, + POINTER_ABSOLUTE => return Err(Refused::Position { x, y }), + other => return Err(Refused::Kind(other)), + }; + Ok(Report { source, buttons, scroll: scroll as i8, motion }) +} diff --git a/lib/input/tests/input.rs b/lib/input/tests/input.rs new file mode 100644 index 00000000000..ec59bb57699 --- /dev/null +++ b/lib/input/tests/input.rs @@ -0,0 +1,177 @@ +//! The owner of input's merge and the one reader of a source's records. + +use toyos_abi::input::{MouseEvent, RawKeyEvent, MOD_RELEASED, MOD_SHIFT, POINTER_ABSOLUTE, POINTER_RELATIVE}; +use toyos_input::wire::{self, Motion, Record, Refused, Report, KEY_LEN, MSG_KEY, MSG_LOST, MSG_POINTER, POINTER_LEN}; +use toyos_input::{rel_scale_for, Merge, Pointer, Slot, State}; + +const SHIFT: u8 = 0xE1; +const A: u8 = 0x04; + +fn press(keycode: u8) -> RawKeyEvent { + RawKeyEvent { keycode, modifiers: 0 } +} + +fn lift(keycode: u8) -> RawKeyEvent { + RawKeyEvent { keycode, modifiers: MOD_RELEASED } +} + +fn slot(n: usize) -> Slot { + Slot::connection(n).expect("a connection the port holds") +} + +fn bytes(event: &MouseEvent) -> [u8; POINTER_LEN] { + toyos_abi::usersafe::bytes(event).try_into().expect("a record's length") +} + +fn relative(source: u8, buttons: u8, dx: i16, dy: i16) -> Report { + Report { source, buttons, scroll: 0, motion: Motion::Relative { dx, dy } } +} + +#[test] +fn a_key_with_no_usage_or_a_modifier_bit_is_refused() { + assert_eq!(wire::key(&[0, 0]), Err(Refused::NoUsage)); + assert_eq!(wire::key(&[0, MOD_RELEASED]), Err(Refused::NoUsage)); + for bit in (0..8).map(|n| 1u8 << n).filter(|&bit| bit != MOD_RELEASED) { + assert_eq!(wire::key(&[A, bit]), Err(Refused::Modifiers(bit)), "modifier bit {bit:#04x}"); + assert_eq!(wire::key(&[A, bit | MOD_RELEASED]), Err(Refused::Modifiers(bit | MOD_RELEASED))); + } + assert_eq!(wire::key(&[A, 0]), Ok(press(A))); + assert_eq!(wire::key(&[A, MOD_RELEASED]), Ok(lift(A))); +} + +#[test] +fn a_pointer_report_past_five_buttons_of_no_kind_or_off_the_surface_is_refused() { + let event = |buttons, kind, x, y| MouseEvent { source: 0, buttons, scroll: 0, kind, x, y }; + for button in 5..8 { + let buttons = 1 << button; + assert_eq!(wire::pointer(&bytes(&event(buttons, POINTER_RELATIVE, 0, 0))), Err(Refused::Buttons(buttons))); + } + assert!(wire::pointer(&bytes(&event(0x1F, POINTER_RELATIVE, 0, 0))).is_ok(), "five buttons"); + for kind in 2..=u8::MAX { + assert_eq!(wire::pointer(&bytes(&event(0, kind, 0, 0))), Err(Refused::Kind(kind))); + } + for (x, y) in [(-1, 0), (0, -1), (i16::MIN, 5)] { + assert_eq!(wire::pointer(&bytes(&event(0, POINTER_ABSOLUTE, x, y))), Err(Refused::Position { x, y })); + } + assert_eq!( + wire::pointer(&bytes(&event(1, POINTER_ABSOLUTE, 32767, 0))).map(|r| r.motion), + Ok(Motion::Absolute { x: 32767, y: 0 }) + ); + assert_eq!( + wire::pointer(&bytes(&event(1, POINTER_RELATIVE, -300, 255))).map(|r| r.motion), + Ok(Motion::Relative { dx: -300, dy: 255 }) + ); +} + +#[test] +fn a_frame_of_another_type_or_length_is_refused() { + assert_eq!(wire::decode(0, &[]), Err(Refused::Type(0))); + assert_eq!(wire::decode(4, &[A, 0]), Err(Refused::Type(4))); + for len in [0, KEY_LEN - 1, KEY_LEN + 1, POINTER_LEN] { + assert_eq!(wire::decode(MSG_KEY, &vec![A; len]), Err(Refused::Length { msg_type: MSG_KEY, len })); + } + for len in [0, KEY_LEN, POINTER_LEN - 1, POINTER_LEN + 1] { + assert_eq!(wire::decode(MSG_POINTER, &vec![0; len]), Err(Refused::Length { msg_type: MSG_POINTER, len })); + } + assert_eq!(wire::decode(MSG_LOST, &[0]), Err(Refused::Length { msg_type: MSG_LOST, len: 1 })); +} + +#[test] +fn every_record_decodes_as_the_frame_it_was_sent_as() { + let records = [ + Record::Key(press(A)), + Record::Key(lift(SHIFT)), + Record::Pointer(relative(3, 0b101, -7, 300)), + Record::Pointer(Report { source: 0, buttons: 0, scroll: -2, motion: Motion::Absolute { x: 32767, y: 9 } }), + Record::Lost, + ]; + for record in records { + let mut out = [0; POINTER_LEN]; + let (msg_type, len) = record.frame(&mut out); + assert_eq!(wire::decode(msg_type, &out[..len]), Ok(record)); + } +} + +#[test] +fn shift_from_one_source_and_a_from_another_arrive_as_a_shifted_a() { + let mut merge = Merge::default(); + assert_eq!(merge.key(slot(0), press(SHIFT)).collect::>(), [RawKeyEvent { keycode: SHIFT, modifiers: MOD_SHIFT }]); + assert_eq!(merge.key(Slot::KERNEL, press(A)).collect::>(), [RawKeyEvent { keycode: A, modifiers: MOD_SHIFT }]); + assert_eq!(merge.key(slot(0), lift(SHIFT)).collect::>(), [RawKeyEvent { keycode: SHIFT, modifiers: MOD_RELEASED }]); + assert_eq!(merge.key(Slot::KERNEL, lift(A)).collect::>(), [RawKeyEvent { keycode: A, modifiers: MOD_RELEASED }]); +} + +#[test] +fn a_repeated_make_from_one_source_delivers_nothing() { + let mut merge = Merge::default(); + assert_eq!(merge.key(slot(1), press(A)).count(), 1); + assert_eq!(merge.key(slot(1), press(A)).count(), 0); + assert_eq!(merge.key(slot(2), press(A)).count(), 0, "a key another source holds"); + assert_eq!(merge.key(slot(1), lift(A)).count(), 0, "still held by the other source"); + assert_eq!(merge.key(slot(2), lift(A)).collect::>(), [lift(A)]); + assert_eq!(merge.key(slot(2), lift(A)).count(), 0, "a release of what is not held"); +} + +#[test] +fn a_source_that_ends_releases_what_it_held_and_nothing_another_holds() { + let mut merge = Merge::default(); + merge.key(slot(0), press(SHIFT)).for_each(drop); + merge.key(slot(0), press(A)).for_each(drop); + merge.key(slot(3), press(SHIFT)).for_each(drop); + assert_eq!(merge.end(slot(0)).collect::>(), [RawKeyEvent { keycode: A, modifiers: MOD_SHIFT | MOD_RELEASED }]); + assert_eq!(merge.end(slot(0)).count(), 0, "it holds nothing now"); + assert_eq!(merge.end(slot(3)).collect::>(), [RawKeyEvent { keycode: SHIFT, modifiers: MOD_RELEASED }]); +} + +#[test] +fn two_pointers_buttons_or_and_ones_empty_report_clears_none_of_the_others() { + let mut pointer = Pointer::new(1920, 1080); + assert_eq!(pointer.report(slot(0), relative(0, 1, 0, 0)).buttons, 1); + assert_eq!(pointer.report(Slot::KERNEL, relative(4, 2, 0, 0)).buttons, 3, "another slot's pointer"); + assert_eq!(pointer.report(Slot::KERNEL, relative(5, 0, 0, 0)).buttons, 3, "a third pointer, holding nothing"); + assert_eq!(pointer.report(Slot::KERNEL, relative(4, 0, 0, 0)).buttons, 1); + assert_eq!(pointer.report(slot(0), relative(1, 4, 0, 0)).buttons, 5, "one slot's two pointers"); +} + +#[test] +fn a_connections_end_clears_its_pointers_buttons_and_no_others() { + let mut pointer = Pointer::new(1920, 1080); + pointer.report(slot(2), relative(0, 1, 0, 0)); + pointer.report(slot(2), relative(9, 4, 0, 0)); + pointer.report(slot(1), relative(0, 2, 0, 0)); + assert_eq!(pointer.end(slot(2)).buttons, 2); + assert_eq!(pointer.end(slot(1)).buttons, 0); +} + +#[test] +fn relative_motion_scales_by_the_screen_and_absolute_motion_sets() { + let (width, height) = (1920, 1080); + let (sx, sy) = rel_scale_for(width, height); + let mut pointer = Pointer::new(width, height); + let start = pointer.report(slot(0), relative(0, 0, 0, 0)); + assert_eq!((start.x, start.y), (16384, 16384), "it starts in the middle"); + let moved = pointer.report(slot(0), relative(0, 0, 10, -3)); + assert_eq!((moved.x, moved.y), (16384 + 10 * sx, 16384 - 3 * sy)); + let set = pointer.report(slot(1), Report { source: 0, buttons: 0, scroll: 1, motion: Motion::Absolute { x: 7, y: 9 } }); + assert_eq!(set, State { buttons: 0, scroll: 1, x: 7, y: 9 }); + let clamped = pointer.report(slot(0), relative(0, 0, i16::MIN, i16::MAX)); + assert_eq!((clamped.x, clamped.y), (0, 32767), "the edges hold it"); + pointer.screen(800, 800); + let square = pointer.report(slot(0), relative(0, 0, 1, -1)); + assert_eq!((square.x, square.y), (64, 32767 - 64)); +} + +/// Relative motion lands in a square space mapped by width and height, so the +/// counts per pixel differ per axis or the pointer skews by the aspect ratio. +#[test] +fn a_count_moves_as_far_across_as_down() { + for (w, h) in [(1920u32, 1200u32), (1024, 768), (3840, 2160), (1200, 1920), (800, 800)] { + let (sx, sy) = rel_scale_for(w, h); + assert!(sx >= 1 && sy >= 1, "{w}x{h} scaled a count to nothing"); + let across = sx as u64 * w as u64; + let down = sy as u64 * h as u64; + // Each scale is a truncated integer, so each product carries under one + // screen dimension of error. + assert!(across.abs_diff(down) < (w + h) as u64, "{w}x{h} moves {across} across and {down} down per count"); + } +} diff --git a/lib/manifest/src/lib.rs b/lib/manifest/src/lib.rs index 5e66f47098e..b7a5e4cb2df 100644 --- a/lib/manifest/src/lib.rs +++ b/lib/manifest/src/lib.rs @@ -158,6 +158,15 @@ fn data_dir(dir: String, write: bool) -> View { pub const RESTARTS: u32 = 3; pub const RESTART_WINDOW_SECS: u64 = 10; +/// One start of a row, as the supervisor asks [`Program::supervised`] about it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Start { + /// It was just spawned. + Spawned, + /// It ended with this exit code. + Ended(i32), +} + pub use toyos_abi::handle::Rights; pub use toyos_abi::syscall::{DeviceRequest, DeviceType}; @@ -274,6 +283,18 @@ pub struct Program { } impl Program { + /// Whether the supervisor acts on `start` of this row. At its spawn, + /// whether a waiter sees it end: it serves ports, which close with an end + /// no restart is owed, or it is `restart`. At its end, whether it is + /// started again: it is `restart`, and it ended other than by exit 0, + /// which a program says only when it has nothing to serve. + pub fn supervised(&self, start: Start) -> bool { + match start { + Start::Spawned => self.restart || !self.serves.is_empty() || !self.roles.is_empty(), + Start::Ended(exit) => self.restart && exit != 0, + } + } + /// It replaces what the machine runs — the swap port's holder, or the idle /// slot's — so it starts only in a login session. pub fn login_only(&self) -> bool { @@ -558,6 +579,47 @@ pub fn parse(text: &str) -> Manifest { mod tests { use super::*; + fn row(serves: &[&str], roles: &[&str], restart: bool) -> Program { + Program { + serves: serves.iter().map(|s| s.to_string()).collect(), + roles: roles.iter().map(|s| s.to_string()).collect(), + restart, + ..Program::default() + } + } + + /// A `restart` row that serves no port has no port to close, and is the + /// one row a waiter for ported starts alone would never see end. + #[test] + fn a_restart_row_with_no_port_is_waited_on() { + assert!(row(&[], &[], true).supervised(Start::Spawned)); + } + + #[test] + fn a_row_with_ports_is_waited_on_and_one_that_is_neither_is_not() { + assert!(row(&["block"], &[], false).supervised(Start::Spawned)); + assert!(row(&[], &["data"], false).supervised(Start::Spawned)); + assert!(!row(&[], &[], false).supervised(Start::Spawned)); + } + + #[test] + fn a_restart_rows_end_other_than_exit_0_is_owed_a_start_and_exit_0_is_not() { + for exit in [1, -1, 101, 139] { + assert!(row(&[], &[], true).supervised(Start::Ended(exit)), "exit {exit}"); + assert!(row(&["block"], &[], true).supervised(Start::Ended(exit)), "exit {exit}"); + } + assert!(!row(&[], &[], true).supervised(Start::Ended(0))); + assert!(!row(&["block"], &[], true).supervised(Start::Ended(0))); + } + + #[test] + fn a_row_that_is_not_restart_is_never_owed_a_start() { + for exit in [0, 1, 139] { + assert!(!row(&["log"], &[], false).supervised(Start::Ended(exit)), "exit {exit}"); + assert!(!row(&[], &[], false).supervised(Start::Ended(exit)), "exit {exit}"); + } + } + fn sample() -> Manifest { Manifest { programs: vec![ diff --git a/sdk/abi/src/input.rs b/sdk/abi/src/input.rs index 68524c03069..2e23438492f 100644 --- a/sdk/abi/src/input.rs +++ b/sdk/abi/src/input.rs @@ -1,6 +1,7 @@ //! Wire-format types for HID input devices (keyboard, mouse). //! -//! These cross the kernel→userland boundary as reads on a device handle. +//! These cross the kernel→userland boundary as reads on a device handle, and +//! a source's connection to the owner of input (`toyos-input`). /// Keyboard modifier flags. pub const MOD_SHIFT: u8 = 1; @@ -10,17 +11,15 @@ pub const MOD_GUI: u8 = 8; pub const MOD_RELEASED: u8 = 0x10; crate::user_safe! { - /// One key transition, as the kernel delivers it through the keyboard handle. + /// One key transition: which physical key moved, by its HID Keyboard-page + /// usage, and which way. There is no layout in it, so there is nothing + /// here that a layout could change. /// - /// The whole of what the kernel knows about a key: which physical key moved, - /// which way, and what the machine's modifiers were when it did. There is no - /// layout in the kernel, so there is nothing here that a layout could change. - /// - /// **`modifiers` is the union across every keyboard the machine has**, which - /// is why it is here rather than derived by whoever translates: Shift held on - /// one keyboard and a letter typed on another must produce a capital, and a - /// surface that reconstructs the mask from the transitions it saw has not seen - /// the ones that arrived while another surface had the focus. + /// **From a source, `modifiers` is `MOD_RELEASED` or nothing**: a source + /// knows one keyboard's keys. The owner of input merges every source's + /// (`toyos_input::Merge`) and hands on each transition with the modifier + /// mask across every keyboard, so Shift held on one keyboard and a letter + /// typed on another is a capital. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct RawKeyEvent { pub keycode: u8, @@ -37,14 +36,31 @@ impl RawKeyEvent { pub fn gui(&self) -> bool { self.modifiers & MOD_GUI != 0 } } +/// [`MouseEvent::kind`]: `x` and `y` are a movement, in the device's counts. +pub const POINTER_RELATIVE: u8 = 0; +/// [`MouseEvent::kind`]: `x` and `y` are a position, each in +/// `0..=`[`POINTER_ABSOLUTE_MAX`]. +pub const POINTER_ABSOLUTE: u8 = 1; +/// The far edge of an absolute axis. +pub const POINTER_ABSOLUTE_MAX: i16 = 32767; + crate::user_safe! { - /// A mouse/tablet event as delivered by the kernel through the mouse handle. - /// Carries absolute coordinates (0–32767) from the USB tablet. - #[derive(Clone, Copy)] + /// One pointer report, as the device gave it: nothing is integrated or + /// merged before the owner of input, which ORs every pointer's buttons + /// and keeps the one position (`toyos_input::Pointer`). + #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct MouseEvent { + /// Which of its sender's pointers this is. + pub source: u8, + /// This pointer's own buttons, in HID boot-mouse order: bit 0 left, + /// 1 right, 2 middle, then two more. pub buttons: u8, + /// Wheel notches, away from the user positive. pub scroll: i8, - pub abs_x: u16, - pub abs_y: u16, + /// [`POINTER_RELATIVE`] or [`POINTER_ABSOLUTE`]. + pub kind: u8, + pub x: i16, + /// Down the screen positive, in either kind. + pub y: i16, } } diff --git a/sdk/keymap/Cargo.toml b/sdk/keymap/Cargo.toml index c06843517f5..122b123cb2e 100644 --- a/sdk/keymap/Cargo.toml +++ b/sdk/keymap/Cargo.toml @@ -1,4 +1,4 @@ -# A member of the host workspace (root `Cargo.toml`), like toyos-ps2: the kernel +# A member of the host workspace (root `Cargo.toml`): the kernel # depends on it by path and its tests run on the host, with no target-triple # juggling. diff --git a/sdk/toyos/src/endow.rs b/sdk/toyos/src/endow.rs index d30ecbc6fc7..51b23edd1e5 100644 --- a/sdk/toyos/src/endow.rs +++ b/sdk/toyos/src/endow.rs @@ -247,6 +247,14 @@ pub fn partition(guid: toyos_abi::part::PartGuid) -> Option { with_prefixed(DEV_PREFIX, name, |label| Endowments::get().take::(label)) } +/// The claim for one legacy ISA function the manifest says this program gets, +/// looked up by the entry's own spelling exactly as [`pci_function`] is. +pub fn isa(set: toyos_abi::syscall::IsaId) -> Option { + let mut buf = [0u8; DeviceRequest::MAX_NAME]; + let name = DeviceRequest::Isa(set).write_name(&mut buf); + with_prefixed(DEV_PREFIX, name, |label| Endowments::get().take::(label)) +} + /// Compose `` on the stack. Nothing in the SDK allocates, and a /// name past the label bound matches nothing rather than being truncated into /// a label that happens to exist. diff --git a/system/compositor/Cargo.toml b/system/compositor/Cargo.toml index 26b6f960a5e..9000141de58 100644 --- a/system/compositor/Cargo.toml +++ b/system/compositor/Cargo.toml @@ -10,6 +10,7 @@ sprite = { path = "sprite" } toyos = { path = "../../sdk/toyos" } toyos-abi = { path = "../../sdk/abi" } toyos-desktop = { path = "desktop" } +toyos-input = { path = "../../lib/input" } toyos-inspect = { path = "../../lib/inspect" } toyos-font = { path = "../../sdk/font" } toyos-manifest = { path = "../../lib/manifest" } diff --git a/system/compositor/desktop/Cargo.toml b/system/compositor/desktop/Cargo.toml index 09b34ce6dad..626c13387c6 100644 --- a/system/compositor/desktop/Cargo.toml +++ b/system/compositor/desktop/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] toyos-abi = { path = "../../../sdk/abi" } +toyos-input = { path = "../../../lib/input" } [lints.rust] warnings = "deny" diff --git a/system/compositor/desktop/src/input.rs b/system/compositor/desktop/src/input.rs index 392853ec0b4..b95ea265743 100644 --- a/system/compositor/desktop/src/input.rs +++ b/system/compositor/desktop/src/input.rs @@ -1,4 +1,5 @@ -use toyos_abi::input::{MouseEvent, RawKeyEvent}; +use toyos_abi::input::RawKeyEvent; +use toyos_input::State; use crate::hit::{hit_test, Hit}; use crate::layout::{set_mode, Desk}; @@ -100,32 +101,14 @@ pub fn key_action(ev: RawKeyEvent, focused: Option, launcher_open: b KeyAction::Forward } -/// One report off the mouse claim. -pub const MOUSE_EVENT_LEN: usize = 6; - -const _: () = { - assert!(core::mem::size_of::() == MOUSE_EVENT_LEN); - assert!(core::mem::offset_of!(MouseEvent, buttons) == 0); - assert!(core::mem::offset_of!(MouseEvent, scroll) == 1); - assert!(core::mem::offset_of!(MouseEvent, abs_x) == 2); - assert!(core::mem::offset_of!(MouseEvent, abs_y) == 4); -}; - -fn decode(b: &[u8]) -> MouseEvent { - MouseEvent { - buttons: b[0], - scroll: b[1] as i8, - abs_x: u16::from_le_bytes([b[2], b[3]]), - abs_y: u16::from_le_bytes([b[4], b[5]]), - } -} - -/// A whole read of the mouse claim, folded into the one thing that happened. +/// What the one pointer did over a batch of its states, folded into the one +/// thing that happened. /// -/// The kernel queues reports and one read drains them, so a batch can contain -/// a press and its release, several scroll notches and a dozen positions. The -/// desktop acts on the *transitions* and the last position: replaying every -/// intermediate one would hit-test against a pointer that is no longer there. +/// A batch is every record the sources sent since the last pass, so it can +/// contain a press and its release, several scroll notches and a dozen +/// positions. The desktop acts on the *transitions* and the last position: +/// replaying every intermediate one would hit-test against a pointer that is +/// no longer there. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] pub struct MouseSample { pub reports: usize, @@ -145,18 +128,14 @@ impl MouseSample { } } -/// Fold a read of the mouse claim, given what the buttons were before it. -/// -/// A trailing partial report is ignored rather than being decoded from -/// whatever follows it in the buffer. -pub fn fold_mouse(prev_buttons: u8, bytes: &[u8]) -> MouseSample { +/// Fold the pointer's `states`, given what the buttons were before them. +pub fn fold_mouse(prev_buttons: u8, states: &[State]) -> MouseSample { let mut s = MouseSample { buttons: prev_buttons, ..MouseSample::default() }; - for chunk in bytes.as_chunks::().0 { - let ev = decode(chunk); + for ev in states { s.reports += 1; s.scroll += ev.scroll as i32; - s.abs_x = ev.abs_x; - s.abs_y = ev.abs_y; + s.abs_x = ev.x; + s.abs_y = ev.y; let was = s.buttons & 1 != 0; let now = ev.buttons & 1 != 0; s.pressed |= now && !was; @@ -166,10 +145,10 @@ pub fn fold_mouse(prev_buttons: u8, bytes: &[u8]) -> MouseSample { s } -/// Full scale of the absolute axes the USB tablet reports on. +/// Full scale of the pointer's absolute axes. const ABS_RANGE: i32 = 32768; -/// Where a tablet's absolute position lands on the screen. +/// Where the pointer's absolute position lands on the screen. pub fn cursor_from_abs(abs_x: u16, abs_y: u16, screen: Rect) -> Point { Point { x: (screen.x0 + abs_x as i32 * screen.w() / ABS_RANGE) @@ -631,22 +610,13 @@ mod tests { assert!(!gate.owed(key(escape, MOD_RELEASED)), "a release was taken twice"); } - fn report(buttons: u8, scroll: i8, x: u16, y: u16) -> [u8; 6] { - let mut b = [0u8; 6]; - b[0] = buttons; - b[1] = scroll as u8; - b[2..4].copy_from_slice(&x.to_le_bytes()); - b[4..6].copy_from_slice(&y.to_le_bytes()); - b + fn report(buttons: u8, scroll: i8, x: u16, y: u16) -> State { + State { buttons, scroll, x, y } } #[test] fn a_batch_folds_to_its_transitions_and_its_last_position() { - let mut buf = [0u8; 18]; - buf[0..6].copy_from_slice(&report(0, 1, 100, 100)); - buf[6..12].copy_from_slice(&report(1, 1, 200, 200)); - buf[12..18].copy_from_slice(&report(0, -1, 300, 400)); - let s = fold_mouse(0, &buf); + let s = fold_mouse(0, &[report(0, 1, 100, 100), report(1, 1, 200, 200), report(0, -1, 300, 400)]); assert_eq!(s.reports, 3); assert!(s.pressed && s.released); assert!(!s.left_held()); @@ -656,19 +626,10 @@ mod tests { #[test] fn a_button_already_down_is_not_a_press() { - let s = fold_mouse(1, &report(1, 0, 10, 10)); + let s = fold_mouse(1, &[report(1, 0, 10, 10)]); assert!(!s.pressed && !s.released && s.left_held()); } - #[test] - fn a_truncated_report_is_not_decoded_from_what_follows_it() { - let mut buf = [0u8; 9]; - buf[0..6].copy_from_slice(&report(1, 0, 7, 9)); - let s = fold_mouse(0, &buf); - assert_eq!(s.reports, 1); - assert_eq!((s.abs_x, s.abs_y), (7, 9)); - } - #[test] fn the_tablet_range_maps_onto_the_screen_and_never_off_it() { let screen = Rect::new(0, 0, 1920, 1080); diff --git a/system/compositor/desktop/src/lib.rs b/system/compositor/desktop/src/lib.rs index 0874ce50676..2521a1c1f43 100644 --- a/system/compositor/desktop/src/lib.rs +++ b/system/compositor/desktop/src/lib.rs @@ -37,7 +37,7 @@ pub use damage::{Damage, MAX_DAMAGE_RECTS}; pub use hit::{hit_test, Hit}; pub use input::{ cursor_from_abs, cursor_style, edge_snap, fold_mouse, key_action, tab_action, CursorStyle, Grab, - Held, KeyAction, MouseSample, PromptGate, Released, TabAction, DRAG_THRESHOLD, MOUSE_EVENT_LEN, + Held, KeyAction, MouseSample, PromptGate, Released, TabAction, DRAG_THRESHOLD, }; pub use layout::{set_mode, Chrome, Desk}; pub use plan::{compose, content_blit, Blit, Layer}; diff --git a/system/compositor/src/main.rs b/system/compositor/src/main.rs index 4aedb0063a0..fa3dafad129 100644 --- a/system/compositor/src/main.rs +++ b/system/compositor/src/main.rs @@ -44,17 +44,21 @@ pub const FLAG_HARDWARE_CURSOR: u32 = 1 << 0; /// the two listeners, `compositor` and `prompt`. pub const FIXED_POLL_HANDLES: u32 = 4; +/// The `input` port's listener and each of its sources. +pub const INPUT_POLL_HANDLES: u32 = 1 + toyos_input::wire::CONNECTIONS as u32; + /// Hard ceiling on live client windows, from the poller rather than from memory. /// /// Every window's handle is registered in the same batch as the four fixed -/// ones, the pending connections and the prompt's two, and [`Poller::MAX_HANDLES`] is the widest set +/// ones, the pending connections, the prompt's two and the input port's, and [`Poller::MAX_HANDLES`] is the widest set /// one poller can carry. Unlike the memory budget this does not move when the /// resolution does, which is why the poller is sized from it: /// `MSG_SET_RESOLUTION` can make windows cheaper mid-run, and a poller sized /// for the old screen would then be too small. At any resolution this machine /// can actually scan out, the memory budget is far below this and is what /// binds. -pub const MAX_WINDOW_SLOTS: u32 = Poller::MAX_HANDLES - FIXED_POLL_HANDLES - MAX_PENDING_CONNS - PROMPT_POLL_HANDLES; +pub const MAX_WINDOW_SLOTS: u32 = + Poller::MAX_HANDLES - FIXED_POLL_HANDLES - MAX_PENDING_CONNS - PROMPT_POLL_HANDLES - INPUT_POLL_HANDLES; /// The prompt's own: its connection waiting for its first frame, and its /// window, which no client's ceiling counts (`toyos_desktop::pending_admits`, diff --git a/system/compositor/src/session.rs b/system/compositor/src/session.rs index 05e14e7981d..d9856313cb4 100644 --- a/system/compositor/src/session.rs +++ b/system/compositor/src/session.rs @@ -13,13 +13,16 @@ use std::time::{Duration, Instant}; use sprite::Sprite; use toyos::endow; -use toyos::ipc::RxStep; +use toyos::ipc::{Connection, FrameRx, RxStep}; use toyos::poller::{Poller, READABLE}; use toyos::port::Acceptor; use toyos::shm::SharedMemory; use toyos::{ipc, system, AsHandle, FramebufferDev, Keyboard, Mouse}; +use toyos_abi::input::RawKeyEvent; use toyos_abi::syscall::DeviceType; use toyos_abi::RawHandle; +use toyos_input::wire::{self, Record, CONNECTIONS, KEEP, KEY_LEN, POINTER_LEN}; +use toyos_input::{Merge, Pointer, Slot, State}; use toyos_desktop::{ cursor_from_abs, cursor_style, fold_mouse, hit_test, key_action, set_mode, tab_action, Chrome, CursorStyle, Damage, Desk, Grab, Held, Hit, KeyAction, Level, Point, PromptGate, Rect, Released, @@ -36,9 +39,18 @@ use crate::render::{self, Assets, BackBuffer, SystemStats, TitleBarIcons}; use crate::stats::FrameTotals; use crate::{ launcher_apps, DOUBLE_CLICK_TIME, DRAIN_BUDGET, FIXED_POLL_HANDLES, - FLAG_HARDWARE_CURSOR, FRAME_INTERVAL, MAX_WINDOW_SLOTS, PROMPT_POLL_HANDLES, + FLAG_HARDWARE_CURSOR, FRAME_INTERVAL, INPUT_POLL_HANDLES, MAX_WINDOW_SLOTS, PROMPT_POLL_HANDLES, }; +/// The most frames one source's turn takes before the others are served. +const SOURCE_FRAMES: usize = 64; + +/// One connection on the `input` port: a source of keys and pointer reports. +struct Source { + conn: Connection, + rx: FrameRx, +} + const _: () = assert!( toyos_inspect::MAX_SNAPSHOT_BYTES == ipc::MAX_FRAME_LEN as usize, "a snapshot is one frame" @@ -106,6 +118,12 @@ pub struct Session { launcher: mpsc::Sender, kb: Keyboard, mouse: Mouse, + /// The `input` port, and the sources on it, by their slot. + inputs: Acceptor, + sources: [Option; CONNECTIONS], + /// Every source's keys and pointers, the kernel's streams among them. + merge: Merge, + pointers: Pointer, poller: Poller, /// The claim, and the thing every screen call is made *through*: closing it @@ -170,6 +188,8 @@ impl Session { .expect("the manifest gives this program the keyboard"); let mouse: Mouse = endow::device(DeviceType::Mouse) .expect("the manifest gives this program the mouse"); + let inputs = endow::acceptor(wire::PORT) + .expect("the manifest declares this program serves `input`"); let fb_dev: FramebufferDev = endow::device(DeviceType::Framebuffer) .expect("the manifest gives this program the framebuffer"); @@ -239,9 +259,13 @@ impl Session { // Sized for the slot ceiling rather than for `max_windows`: the batch // between two `wait` calls is the four fixed registrations, one per // live window - // and one per pending connection, the prompt's two beside them, and + // and one per pending connection, the prompt's two and the input + // port's beside them, and // `MSG_SET_RESOLUTION` can raise `max_windows` mid-run. - let poller = Poller::new(FIXED_POLL_HANDLES + MAX_WINDOW_SLOTS + MAX_PENDING_CONNS + PROMPT_POLL_HANDLES); + let poller = Poller::new( + FIXED_POLL_HANDLES + MAX_WINDOW_SLOTS + MAX_PENDING_CONNS + PROMPT_POLL_HANDLES + INPUT_POLL_HANDLES, + ); + poller.watch(&inputs, READABLE, inputs.as_handle().0 as u64); poller.watch(&kb, READABLE, kb.as_handle().0 as u64); poller.watch(&mouse, READABLE, mouse.as_handle().0 as u64); poller.watch(&acceptor, READABLE, acceptor.as_handle().0 as u64); @@ -267,6 +291,10 @@ impl Session { launcher, kb, mouse, + inputs, + sources: Default::default(), + merge: Merge::default(), + pointers: Pointer::new(fb_info.width, fb_info.height), poller, fb_dev, fb_info, @@ -340,6 +368,10 @@ impl Session { let kb_ready = self.is_ready(self.kb.as_handle()); let mouse_ready = self.is_ready(self.mouse.as_handle()); + let inputs_ready = self.is_ready(self.inputs.as_handle()); + let sources_ready: Vec = (0..CONNECTIONS) + .filter(|&n| self.sources[n].as_ref().is_some_and(|s| self.is_ready(s.conn.as_handle()))) + .collect(); let accept_ready = self.is_ready(self.acceptor.as_handle()); let prompt_ready = self.is_ready(self.prompts.as_handle()); let client_ready = self.stack.iter().any(|w| self.is_ready(w.client.conn.as_handle())) @@ -359,7 +391,7 @@ impl Session { } self.pending.retain(|p| now.duration_since(p.since) < HANDSHAKE_TIMEOUT); - if !kb_ready && !mouse_ready && !accept_ready && !prompt_ready && !client_ready { + if !kb_ready && !mouse_ready && !inputs_ready && sources_ready.is_empty() && !accept_ready && !prompt_ready && !client_ready { return false; } @@ -370,6 +402,12 @@ impl Session { if mouse_ready { self.pointer(); } + if inputs_ready { + self.admit(); + } + for n in sources_ready { + self.source(n); + } if accept_ready { self.accept(false); } @@ -380,7 +418,7 @@ impl Session { self.dispatch(frames); self.reap(); self.prompt_gone(); - self.rearm(kb_ready, mouse_ready, accept_ready, prompt_ready); + self.rearm(kb_ready, mouse_ready, inputs_ready, accept_ready, prompt_ready); true } @@ -392,88 +430,118 @@ impl Session { self.screen.pixel_format_raw() } + /// The kernel's keyboard stream, one source to the merge. fn keys(&mut self) { - const EVENT: usize = std::mem::size_of::(); - let mut buf = [0u8; 8 * EVENT]; + let mut buf = [0u8; 8 * KEY_LEN]; // Never read blocking here. The kernel wakes only when a report queued // an event, so readiness and `has_data` agree — but a blocking read on // an empty queue parks the compositor until the next real key, and one // spurious wake anywhere would freeze it. let n = self.kb.read_nonblock(&mut buf).unwrap_or(0); - let (events, torn) = buf[..n].as_chunks::(); + let (events, torn) = buf[..n].as_chunks::(); assert!(torn.is_empty(), "compositor: the keyboard read {n} bytes, not whole events"); for raw in events { - let event = ipc::decode_payload::(raw) - .expect("a chunk is exactly one event long"); - let focused = self.stack.focused(); - let action = key_action( - event.into(), - focused.map(|i| self.stack[i].mode), - self.launcher_open, - self.prompt.is_some(), - ); - match action { - KeyAction::Ignore => {} - KeyAction::Forward => { - // While a prompt is up it is the focused window, and is - // given what its gate lets through and nothing else is. - let given = match &mut self.prompt { - Some((_, gate)) => gate.key(event.into()), - None => !self.owed.owed(event.into()), - }; - if !given { - continue; - } - if let Some(i) = focused { - deliver(&mut self.dead, &self.stack[i], window::MSG_KEY_INPUT, &event); - } - } - KeyAction::CloseLauncher => { - self.launcher_open = false; - self.damage.add(self.launcher_rect()); - } - KeyAction::CycleFocus => { - if self.stack.cycle() { - self.damage_all(); - } + match wire::key(raw) { + Ok(key) => self.merged_key(Slot::KERNEL, key), + Err(why) => eprintln!("compositor: the kernel's keyboard stream carried {raw:02x?}, refused: {why:?}"), + } + } + } + + /// `key` from `slot`, and whatever it changed of the merged keyboard. + fn merged_key(&mut self, slot: Slot, key: RawKeyEvent) { + let delivered: Vec = self.merge.key(slot, key).collect(); + for event in delivered { + self.key(event); + } + } + + /// One transition of the merged keyboard. + fn key(&mut self, raw: RawKeyEvent) { + let event = window::KeyEvent::from(raw); + let focused = self.stack.focused(); + let action = key_action( + event.into(), + focused.map(|i| self.stack[i].mode), + self.launcher_open, + self.prompt.is_some(), + ); + match action { + KeyAction::Ignore => {} + KeyAction::Forward => { + // While a prompt is up it is the focused window, and is + // given what its gate lets through and nothing else is. + let given = match &mut self.prompt { + Some((_, gate)) => gate.key(event.into()), + None => !self.owed.owed(event.into()), + }; + if !given { + return; } - KeyAction::SpawnTerminal => self.launch("/system/bin/terminal"), - KeyAction::Paste => { - if let Some(i) = focused { - self.paste(i); - } + if let Some(i) = focused { + deliver(&mut self.dead, &self.stack[i], window::MSG_KEY_INPUT, &event); } - // The three that move a window. Each damages the rect it is - // vacating first, because that is only knowable before it - // moves. - KeyAction::SetMode(mode) => { - let Some(idx) = focused else { continue }; - self.damage.add(self.stack[idx].frame(&self.desk.chrome)); - self.retarget(idx, mode); - self.damage_all(); - } - KeyAction::Minimize => { - let Some(idx) = focused else { continue }; - self.damage.add(self.stack[idx].frame(&self.desk.chrome)); - self.stack[idx].minimized = true; + } + KeyAction::CloseLauncher => { + self.launcher_open = false; + self.damage.add(self.launcher_rect()); + } + KeyAction::CycleFocus => { + if self.stack.cycle() { self.damage_all(); } - KeyAction::CloseFocused => { - let Some(idx) = focused else { continue }; - self.damage.add(self.stack[idx].frame(&self.desk.chrome)); - let win = self.stack.remove(idx); - note_closed("GUI+Q", &win, self.stack.len()); - let _ = win.client.conn.try_signal(window::MSG_WINDOW_CLOSE); - self.damage_all(); + } + KeyAction::SpawnTerminal => self.launch("/system/bin/terminal"), + KeyAction::Paste => { + if let Some(i) = focused { + self.paste(i); } } + // The three that move a window. Each damages the rect it is + // vacating first, because that is only knowable before it + // moves. + KeyAction::SetMode(mode) => { + let Some(idx) = focused else { return }; + self.damage.add(self.stack[idx].frame(&self.desk.chrome)); + self.retarget(idx, mode); + self.damage_all(); + } + KeyAction::Minimize => { + let Some(idx) = focused else { return }; + self.damage.add(self.stack[idx].frame(&self.desk.chrome)); + self.stack[idx].minimized = true; + self.damage_all(); + } + KeyAction::CloseFocused => { + let Some(idx) = focused else { return }; + self.damage.add(self.stack[idx].frame(&self.desk.chrome)); + let win = self.stack.remove(idx); + note_closed("GUI+Q", &win, self.stack.len()); + let _ = win.client.conn.try_signal(window::MSG_WINDOW_CLOSE); + self.damage_all(); + } } } + /// The kernel's mouse stream, one source to the merge. fn pointer(&mut self) { - let mut buf = [0u8; 512]; + let mut buf = [0u8; 64 * POINTER_LEN]; let n = self.mouse.read_nonblock(&mut buf).unwrap_or(0); - let sample = fold_mouse(self.prev_buttons, &buf[..n]); + let (reports, torn) = buf[..n].as_chunks::(); + assert!(torn.is_empty(), "compositor: the mouse read {n} bytes, not whole reports"); + let mut states = Vec::with_capacity(reports.len()); + for raw in reports { + match wire::pointer(raw) { + Ok(report) => states.push(self.pointers.report(Slot::KERNEL, report)), + Err(why) => eprintln!("compositor: the kernel's mouse stream carried {raw:02x?}, refused: {why:?}"), + } + } + self.motion(&states); + } + + /// What the merged pointer did over `states`. + fn motion(&mut self, states: &[State]) { + let sample = fold_mouse(self.prev_buttons, states); if sample.reports == 0 { return; } @@ -683,6 +751,71 @@ impl Session { } } + /// A source's connection off `input`, in the first free slot, or refused + /// by name when every slot is taken. + fn admit(&mut self) { + match self.inputs.accept() { + Err(e) => eprintln!("compositor: an input source could not be accepted ({e:?})"), + Ok(conn) => match self.sources.iter().position(Option::is_none) { + None => eprintln!("compositor: refusing an input source — {CONNECTIONS} are connected"), + Some(n) => { + self.poller.watch(&conn, READABLE, conn.as_handle().0 as u64); + self.sources[n] = Some(Source { conn, rx: FrameRx::new() }); + } + }, + } + } + + /// What source `n` sent, each frame whole; one that does not decode, or + /// ends, drops it, and everything it held is released. + fn source(&mut self, n: usize) { + let slot = Slot::connection(n).expect("a source's index is a slot"); + let mut states = Vec::new(); + let mut ended = None; + for _ in 0..SOURCE_FRAMES { + let source = self.sources[n].as_mut().expect("a ready source is held"); + let record = match source.rx.pump(&source.conn) { + RxStep::Idle => break, + RxStep::Eof => { + ended = Some("its connection ended".to_string()); + break; + } + RxStep::Malformed => { + ended = Some("a frame no protocol here can make".to_string()); + break; + } + RxStep::Frame { msg_type, payload_len } => wire::decode(msg_type, source.rx.payload(payload_len)), + }; + match record { + Ok(Record::Key(key)) => self.merged_key(slot, key), + Ok(Record::Pointer(report)) => states.push(self.pointers.report(slot, report)), + Ok(Record::Lost) => { + let released: Vec = self.merge.end(slot).collect(); + released.into_iter().for_each(|key| self.key(key)); + states.push(self.pointers.end(slot)); + } + Err(why) => { + ended = Some(format!("a record refused: {why:?}")); + break; + } + } + } + match ended { + None => { + let conn = &self.sources[n].as_ref().expect("a ready source is held").conn; + self.poller.watch(conn, READABLE, conn.as_handle().0 as u64); + } + Some(why) => { + eprintln!("compositor: dropping input source {n} — {why}"); + self.sources[n] = None; + let released: Vec = self.merge.end(slot).collect(); + released.into_iter().for_each(|key| self.key(key)); + states.push(self.pointers.end(slot)); + } + } + self.motion(&states); + } + /// One connection off `compositor`, or off `prompt` where `prompt`. fn accept(&mut self, prompt: bool) { // `accept` installs a descriptor, so it answers `ResourceExhausted` on @@ -1161,6 +1294,7 @@ impl Session { self.back = BackBuffer::new(self.screen.width(), self.screen.height(), self.pixel_format()); self.desk = desk_of(&self.screen, &self.font, self.apps.len()); + self.pointers.screen(info.width, info.height); // What a window costs moved, so what we can afford moved with it. // Windows already open are left alone if the new figure is below their // count — the cap gates creation, it does not evict. @@ -1209,7 +1343,10 @@ impl Session { } /// Re-arm the one-shot poll registrations for every handle that fired. - fn rearm(&mut self, kb: bool, mouse: bool, acceptor: bool, prompts: bool) { + fn rearm(&mut self, kb: bool, mouse: bool, inputs: bool, acceptor: bool, prompts: bool) { + if inputs { + self.poller.watch(&self.inputs, READABLE, self.inputs.as_handle().0 as u64); + } if prompts { self.poller.watch(&self.prompts, READABLE, self.prompts.as_handle().0 as u64); } diff --git a/system/console/Cargo.toml b/system/console/Cargo.toml index 17bc46cd30b..53f4a50e716 100644 --- a/system/console/Cargo.toml +++ b/system/console/Cargo.toml @@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0" terminal = { path = "../terminal" } toyos = { path = "../../sdk/toyos" } toyos-abi = { path = "../../sdk/abi" } +toyos-input = { path = "../../lib/input" } toyos-font = { path = "../../sdk/font" } toyos-window = { path = "../../sdk/window" } toyos-logstream = { path = "../../lib/logstream" } diff --git a/system/console/src/main.rs b/system/console/src/main.rs index e1d33386e26..fe5208f3115 100644 --- a/system/console/src/main.rs +++ b/system/console/src/main.rs @@ -30,13 +30,17 @@ use std::os::toyos::process::CommandExt; use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command}; use terminal::Console; +use toyos::ipc::{Connection, FrameRx, RxStep}; use toyos::poller::{Poller, READABLE}; use toyos::shm::SharedMemory; use toyos::endow; use toyos::port::{self, Connector}; use toyos::surface::{self, Delivery, Host, Notice}; use toyos::{FramebufferDev, Keyboard, Pipe}; +use toyos_abi::input::RawKeyEvent; use toyos_abi::syscall::{DeviceType, SyscallError}; +use toyos_input::wire::{self, Record, CONNECTIONS, KEEP, KEY_LEN}; +use toyos_input::{Merge, Slot}; use toyos_logstream::{Lines, Showing, Source}; use window::Screen; @@ -47,6 +51,9 @@ const FONT: &str = "/system/share/fonts/JetBrainsMono-Regular-8x16.font"; const KEY_PAGE_UP: u8 = 0x4B; const KEY_PAGE_DOWN: u8 = 0x4E; +/// The most frames one input source's turn takes. +const SOURCE_FRAMES: usize = 64; + /// The most of the boot so far [`seed_tail`] will look at. /// /// **A bound on this program's work.** The scrollback bound below is what @@ -199,6 +206,12 @@ fn main() { let kb: Keyboard = endow::device(DeviceType::Keyboard) .expect("the manifest gives this program the keyboard"); + // The `input` port and its sources, merged with the kernel's keyboard + // stream. Their pointer reports are read and moved by nothing: this + // program draws no pointer. + let inputs = endow::acceptor(wire::PORT).expect("the manifest declares this program serves `input`"); + let mut sources: [Option<(Connection, FrameRx)>; CONNECTIONS] = Default::default(); + let mut merge = Merge::default(); // What the panel cost, on a machine whose only instrument is the panel. // The seed is the heaviest thing this program ever draws — a screenful of // log per scrolled row — so a boot that felt slow says so here. @@ -210,14 +223,18 @@ fn main() { ); // The declared set: the shell's two output pipes, the keyboard, the log, - // and this console's own surface listener and its clients. - let poller = Poller::new(4 + Host::POLL_HANDLES); + // this console's own surface listener and its clients, and the `input` + // port's listener and sources. + let poller = Poller::new(4 + Host::POLL_HANDLES + 1 + CONNECTIONS as u32); const TOKEN_STDOUT: u64 = 0; const TOKEN_STDERR: u64 = 1; const TOKEN_KEYBOARD: u64 = 2; const TOKEN_LISTEN: u64 = 3; const TOKEN_CLIENT: u64 = 4; const TOKEN_LOG: u64 = 5; + const TOKEN_INPUT: u64 = 6; + /// Source `n`'s is this plus `n`. + const TOKEN_SOURCE: u64 = 7; // The shell's unfinished line — a prompt, and what is being typed at it — // as it wrote it, for a line of the log to be drawn above. @@ -233,8 +250,14 @@ fn main() { if let Ok(log) = &log { poller.watch(&log.pipe, READABLE, TOKEN_LOG); } + poller.watch(&inputs, READABLE, TOKEN_INPUT); + for (n, source) in sources.iter().enumerate() { + if let Some((conn, _)) = source { + poller.watch(conn, READABLE, TOKEN_SOURCE + n as u64); + } + } - let mut ready = [false; 6]; + let mut ready = [false; TOKEN_SOURCE as usize + CONNECTIONS]; poller.wait(1, u64::MAX, |token| { if (token as usize) < ready.len() { ready[token as usize] = true; @@ -317,45 +340,94 @@ fn main() { } } + // Every transition of the merged keyboard this turn made, in order. + let mut keys: Vec = Vec::new(); + if ready[TOKEN_INPUT as usize] { + match inputs.accept() { + Err(e) => eprintln!("console: an input source could not be accepted ({e:?})"), + Ok(conn) => match sources.iter().position(Option::is_none) { + None => eprintln!("console: refusing an input source — {CONNECTIONS} are connected"), + Some(n) => sources[n] = Some((conn, FrameRx::new())), + }, + } + } + for n in 0..CONNECTIONS { + if !ready[TOKEN_SOURCE as usize + n] { + continue; + } + let slot = Slot::connection(n).expect("a source's index is a slot"); + let Some((conn, rx)) = &mut sources[n] else { continue }; + let mut ended = None; + // A bounded turn, so a source with a frame always ready cannot + // keep the shell's output from being drawn. + for _ in 0..SOURCE_FRAMES { + let record = match rx.pump(conn) { + RxStep::Idle => break, + RxStep::Eof => { + ended = Some("its connection ended".to_string()); + break; + } + RxStep::Malformed => { + ended = Some("a frame no protocol here can make".to_string()); + break; + } + RxStep::Frame { msg_type, payload_len } => wire::decode(msg_type, rx.payload(payload_len)), + }; + match record { + Ok(Record::Key(key)) => keys.extend(merge.key(slot, key)), + Ok(Record::Pointer(_)) => {} + Ok(Record::Lost) => keys.extend(merge.end(slot)), + Err(why) => { + ended = Some(format!("a record refused: {why:?}")); + break; + } + } + } + if let Some(why) = ended { + eprintln!("console: dropping input source {n} — {why}"); + sources[n] = None; + keys.extend(merge.end(slot)); + } + } if ready[TOKEN_KEYBOARD as usize] { - let mut events = [toyos_abi::input::RawKeyEvent { keycode: 0, modifiers: 0 }; 16]; - let buf = unsafe { - std::slice::from_raw_parts_mut( - events.as_mut_ptr() as *mut u8, - std::mem::size_of_val(&events), - ) - }; + let mut buf = [0u8; 16 * KEY_LEN]; // Non-blocking for the reason `Keyboard::read_nonblock` documents: // an event loop that can park on an empty queue is a frozen screen. - let n = kb.read_nonblock(buf).unwrap_or(0); - for &event in &events[..n / std::mem::size_of::()] { - // A client that asked for the keys gets the transition whole, - // releases included, and the translator is left where it is. - if host.deliver(event) == Delivery::Sent { - continue; + let n = kb.read_nonblock(&mut buf).unwrap_or(0); + for raw in buf[..n].as_chunks::().0 { + match wire::key(raw) { + Ok(key) => keys.extend(merge.key(Slot::KERNEL, key)), + Err(why) => eprintln!("console: the kernel's keyboard stream carried {raw:02x?}, refused: {why:?}"), } - if !event.pressed() { - continue; + } + } + for event in keys { + // A client that asked for the keys gets the transition whole, + // releases included, and the translator is left where it is. + if host.deliver(event) == Delivery::Sent { + continue; + } + if !event.pressed() { + continue; + } + match event.keycode { + // Unchorded, unlike a windowed terminal's Shift+PageUp: + // nothing in this image reads PageUp, and most of what the + // panel has to show is the kernel log seeded above the + // prompt. A scrollback that needs a chord is one the owner + // does not reach for with a laptop in his hands. + KEY_PAGE_UP => { + console.scroll_view_up(page_rows); + painted = true; } - match event.keycode { - // Unchorded, unlike a windowed terminal's Shift+PageUp: - // nothing in this image reads PageUp, and most of what the - // panel has to show is the kernel log seeded above the - // prompt. A scrollback that needs a chord is one the owner - // does not reach for with a laptop in his hands. - KEY_PAGE_UP => { - console.scroll_view_up(page_rows); - painted = true; - } - KEY_PAGE_DOWN => { - console.scroll_view_down(page_rows); - painted = true; - } - usage => { - let text = translator.press(usage, window::KeyEvent::from(event).mods()); - if !text.is_empty() { - shell.stdin.write_all(text.as_bytes()).ok(); - } + KEY_PAGE_DOWN => { + console.scroll_view_down(page_rows); + painted = true; + } + usage => { + let text = translator.press(usage, window::KeyEvent::from(event).mods()); + if !text.is_empty() { + shell.stdin.write_all(text.as_bytes()).ok(); } } } diff --git a/system/ps2server/Cargo.toml b/system/ps2server/Cargo.toml new file mode 100644 index 00000000000..c61a110d2b9 --- /dev/null +++ b/system/ps2server/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "ps2server" +description = "The i8042 PS/2 controller, served from userland: its probe, the keyboard's and the pointer's decode, and their records to the owner of input." +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" + +[dependencies] +toyos = { path = "../../sdk/toyos" } +toyos-abi = { path = "../../sdk/abi" } +toyos-input = { path = "../../lib/input" } +# The device's bytes are input from outside this program's trust, so their +# decode is a crate of its own. +toyos-ps2 = { path = "ps2" } + +[package.metadata.toyos.host] +exempt.owns = "ToyOS's i8042, claimed from the kernel with the two lines it raises" diff --git a/kernel/ps2/Cargo.toml b/system/ps2server/ps2/Cargo.toml similarity index 55% rename from kernel/ps2/Cargo.toml rename to system/ps2server/ps2/Cargo.toml index 954c719cd04..67f93a76abe 100644 --- a/kernel/ps2/Cargo.toml +++ b/system/ps2server/ps2/Cargo.toml @@ -1,5 +1,6 @@ -# A member of the host workspace (root `Cargo.toml`): the kernel depends on it -# by path and its tests run on the host, with no target-triple juggling. +# A member of the host workspace (root `Cargo.toml`): /system/bin/ps2server +# depends on it by path and its tests run on the host, with no target-triple +# juggling. [package] name = "toyos-ps2" diff --git a/kernel/ps2/src/key.rs b/system/ps2server/ps2/src/key.rs similarity index 90% rename from kernel/ps2/src/key.rs rename to system/ps2server/ps2/src/key.rs index 0930f87c6c1..95694bb69c4 100644 --- a/kernel/ps2/src/key.rs +++ b/system/ps2server/ps2/src/key.rs @@ -1,8 +1,8 @@ //! Scancode set 1 → HID usage. //! //! Set 1 is what arrives when the keyboard is in set 2 and the controller's -//! translation bit is on — Linux's default configuration, and therefore the -//! best-trodden path on an EC we cannot debug. The translation table lives in +//! translation bit is on, the mode IBM gives for PC compatibility (84F9735, +//! "Keyboard and Auxiliary Device Controller", page 10). The translation table lives in //! the controller, so it costs this crate nothing; what is left is three //! states (base, `0xE0`, `0xE1`) and two 128-byte tables. @@ -106,12 +106,10 @@ enum State { /// Set-1 scancodes in, key transitions out. /// -/// Deliberately holds no record of what is down. The kernel's `keyboard` -/// module keeps one held-set across every keyboard in the machine and drops -/// a transition to a state a usage is already in, so a typematic repeat -/// costs nothing there. A second held-set here would be derived state that -/// can disagree with it — and would wedge a key in the one case where they -/// do: usage released on the *other* keyboard while this one still holds it. +/// Deliberately holds no record of what is down. The owner of input keeps +/// one held set per source (`toyos_input::Merge`) and drops a transition to a +/// state a usage is already in, so a typematic repeat costs nothing there. A +/// second held set here would be derived state that can disagree with it. pub struct KeyDecoder { state: State, } @@ -169,8 +167,8 @@ impl KeyDecoder { // **So a keyboard that resets itself is undetectable on this // wire, and that is a property of the mode rather than of this // decoder.** The driver runs the keyboard in set 2 with the - // controller translating to set 1 — Linux's default and the - // best-trodden EC path — and `0xAA`, the BAT-complete byte a + // controller translating to set 1, the mode IBM gives for PC + // compatibility, and `0xAA`, the BAT-complete byte a // self-reset sends, is bit-identical to `0x2A | 0x80`. The // T14's EC does reset the keyboard after suspend and after a // lid event, so this is reached on real hardware rather than in @@ -180,7 +178,7 @@ impl KeyDecoder { // Shift *release*, which is accidentally the right direction // for the one state that could stick. Untested on metal. If it // does bite, the answer is a controller-side reconnect probe — - // `0xF2` identify on a timer, from `kernel/src/arch/x86_64/i8042/` + // `0xF2` identify on a timer, from `/system/bin/ps2server` // — and never a wire heuristic, because no wire heuristic // exists. 0x00 | 0xFF => KeyOutcome::Lost, diff --git a/kernel/ps2/src/lib.rs b/system/ps2server/ps2/src/lib.rs similarity index 76% rename from kernel/ps2/src/lib.rs rename to system/ps2server/ps2/src/lib.rs index f95b8175088..6ab86e6c10f 100644 --- a/kernel/ps2/src/lib.rs +++ b/system/ps2server/ps2/src/lib.rs @@ -8,9 +8,9 @@ //! a bug that is invisible in QEMU and un-single-steppable on the laptop //! this exists for. //! -//! The kernel side of the driver (`kernel/src/arch/x86_64/i8042/`) owns the -//! controller, the interrupt and the queues. Nothing in here touches -//! hardware, allocates, or knows what a lock is. +//! `/system/bin/ps2server` owns the controller, the interrupt and the +//! connection to the owner of input. Nothing in here touches hardware, +//! allocates, or knows what a lock is. #![no_std] diff --git a/kernel/ps2/src/mouse.rs b/system/ps2server/ps2/src/mouse.rs similarity index 98% rename from kernel/ps2/src/mouse.rs rename to system/ps2server/ps2/src/mouse.rs index 55519873e1d..a8469365877 100644 --- a/kernel/ps2/src/mouse.rs +++ b/system/ps2server/ps2/src/mouse.rs @@ -49,7 +49,7 @@ pub enum MouseOutcome { /// returns to `Head`, and stays misaligned for as long as the motion lasts. /// /// Timing settles it. A PS/2 byte is 11 bits at 10–16.7 kHz, so a 3-byte packet -/// occupies at most ~3.3 ms; the driver programs 100 samples/s, so the line is +/// occupies at most ~3.3 ms; a device reports 100 samples/s after its reset, so the line is /// then idle for ~6.7 ms. 5 ms falls in that trough and nowhere else, which /// makes an idle gap the frame delimiter the format itself lacks. pub const PACKET_GAP_NS: u64 = 5_000_000; diff --git a/kernel/ps2/tests/decode.rs b/system/ps2server/ps2/tests/decode.rs similarity index 100% rename from kernel/ps2/tests/decode.rs rename to system/ps2server/ps2/tests/decode.rs diff --git a/kernel/ps2/tests/fuzz.rs b/system/ps2server/ps2/tests/fuzz.rs similarity index 100% rename from kernel/ps2/tests/fuzz.rs rename to system/ps2server/ps2/tests/fuzz.rs diff --git a/system/ps2server/src/bus.rs b/system/ps2server/src/bus.rs new file mode 100644 index 00000000000..598ed4e9c75 --- /dev/null +++ b/system/ps2server/src/bus.rs @@ -0,0 +1,142 @@ +//! The controller's two ports, and the rules IBM puts on every access to +//! them. +//! +//! Every citation is IBM's *Personal System/2 Hardware Interface Technical +//! Reference — Common Interfaces* (84F9735), its "Keyboard and Auxiliary +//! Device Controller" section by its own page: **C**. +//! +//! - **A byte is read 7 µs after the status read that showed it** (C 14). +//! - **A byte is written only with the input and output buffers both empty** +//! (C 14). What was waiting is handed to the caller, which decides whose it +//! is. +//! - **Every bounded wait looks at its condition once more after its +//! deadline**, so a preempted reader lengthens a wait and never turns a late +//! answer into silence. + +/// Status: the output buffer holds a byte for the system (C 7). +pub const OBF: u8 = 1 << 0; +/// Status: the input buffer holds a byte the controller has not taken. +pub const IBF: u8 = 1 << 1; +/// Status: the byte in the output buffer is the auxiliary device's. +pub const AUXB: u8 = 1 << 5; + +/// What the status port reads where nothing decodes it. +pub const FLOATING: u8 = 0xFF; + +/// Controller commands (C 11-13). +pub const READ_COMMAND_BYTE: u8 = 0x20; +pub const WRITE_COMMAND_BYTE: u8 = 0x60; +pub const DISABLE_AUX: u8 = 0xA7; +pub const ENABLE_AUX: u8 = 0xA8; +pub const DISABLE_KEYBOARD: u8 = 0xAD; +pub const ENABLE_KEYBOARD: u8 = 0xAE; +pub const WRITE_AUX: u8 = 0xD4; + +/// The command byte (C 11). +pub const CB_KEYBOARD_IRQ: u8 = 1 << 0; +pub const CB_AUX_IRQ: u8 = 1 << 1; +/// Placed in the status register's System Flag; 0 after a power-on reset. +pub const CB_SYSTEM_FLAG: u8 = 1 << 2; +/// "Disable Keyboard": the keyboard interface's clock held low, as `0xAD` +/// sets it and `0xAE` clears it (C 12). +pub const CB_KEYBOARD_DISABLED: u8 = 1 << 4; +pub const CB_AUX_DISABLED: u8 = 1 << 5; +/// The keyboard's bytes translated to the PC-compatible codes (C 10). +pub const CB_TRANSLATE: u8 = 1 << 6; + +/// C 14's wait between a status read showing a byte and the read of it. +pub const READ_DELAY_NS: u64 = 7_000; + +/// The controller's ports and the clock, as ps2server reaches them: the +/// machine's through its `isa` claim (`crate::hw`), or a test's. +pub trait Controller { + /// Port 0x64, read. + fn status(&mut self) -> u8; + /// Port 0x60, read. + fn data(&mut self) -> u8; + /// Port 0x64, written. + fn command(&mut self, byte: u8); + /// Port 0x60, written. + fn write(&mut self, byte: u8); + /// Nanoseconds on the clock the claim's records are stamped with. + fn now(&mut self) -> u64; + /// The claim's interrupts since it was last asked, waiting for one until + /// `deadline`; `None` for none by then, looked for once more after it. + fn wait(&mut self, deadline: u64) -> Option; +} + +/// One record of the claim's interrupts. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Irq { + /// When the first of them landed. + pub first_nanos: u64, +} + +/// A byte read off the output buffer, and whose it was. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Byte { + pub value: u8, + pub aux: bool, +} + +/// Which port a write is for. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Port { + Command, + Data, +} + +/// The controller never took a write: its input buffer stayed full, or its +/// output buffer kept filling, until the deadline. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct NotTaken { + pub status: u8, +} + +impl Bus for C {} + +/// C 14's rules over any [`Controller`]. +pub trait Bus: Controller { + /// The byte in the output buffer, if the status says there is one. + fn poll(&mut self) -> Option { + let status = self.status(); + let shown = self.now(); + (status & OBF != 0).then(|| Byte { value: self.read_after(shown), aux: status & AUXB != 0 }) + } + + /// Port 0x60, `READ_DELAY_NS` after `shown`, when a status read showed it full. + fn read_after(&mut self, shown: u64) -> u8 { + while self.now() < shown + READ_DELAY_NS { + core::hint::spin_loop(); + } + self.data() + } + + /// Write `byte` to `port` once both buffers are empty, handing `found` + /// every byte that was waiting first. + fn send(&mut self, port: Port, byte: u8, deadline: u64, found: &mut impl FnMut(Byte)) -> Result<(), NotTaken> { + loop { + let past = self.now() >= deadline; + let status = self.status(); + let shown = self.now(); + if status & OBF != 0 { + found(Byte { value: self.read_after(shown), aux: status & AUXB != 0 }); + if past { + return Err(NotTaken { status }); + } + continue; + } + if status & IBF == 0 { + match port { + Port::Command => self.command(byte), + Port::Data => self.write(byte), + } + return Ok(()); + } + if past { + return Err(NotTaken { status }); + } + core::hint::spin_loop(); + } + } +} diff --git a/system/ps2server/src/fake.rs b/system/ps2server/src/fake.rs new file mode 100644 index 00000000000..12ea3797d6f --- /dev/null +++ b/system/ps2server/src/fake.rs @@ -0,0 +1,467 @@ +//! A scripted i8042, its keyboard and its auxiliary device, for the probe's +//! and the stream's tests: each shape is a machine's as IBM's references and +//! the benches' readings give it, and every IBM rule the controller states is +//! a panic here when ps2server breaks it. +//! +//! - **C 14**: a data read sooner than 7 µs after the status read that showed +//! the byte; a write while the input or the output buffer is full. +//! - **C 9**: a controller command C does not list. The commands this fake +//! models are the ones ps2server's design sends; one C lists and no shape +//! models is refused too, by its own message. +//! - **C 12**: `0x20` written while the keyboard interface is enabled, where +//! its answer could be a keystroke. +//! - **C 11**: a device's answer read before the interrupt it raised was taken. +//! +//! The keyboard interface is command-byte bit 4, which `0xAD` sets, `0xAE` +//! clears and `0x60` writes (C 11, C 12); while it is set the keyboard holds +//! what it would send (K88 39). + +use std::collections::VecDeque; + +use crate::bus::{Controller, Irq, AUXB, IBF, OBF, READ_DELAY_NS}; + +const MS: u64 = 1_000_000; +const US: u64 = 1_000; + +/// How long the controller takes a written byte. +const TAKES: u64 = 5 * US; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Src { + Kbd, + Aux, + Controller, +} + +#[derive(Clone, Copy, Debug)] +struct Loaded { + value: u8, + src: Src, + /// An answer to a command, which is read only after its interrupt. + answer: bool, + raised: bool, + taken: bool, +} + +#[derive(Clone, Copy, Debug)] +struct Queued { + ready: u64, + value: u8, + answer: bool, +} + +/// What a device does with its reset: `0xFA` this long after the command, +/// then `then` this long after the `0xFA`. +#[derive(Clone, Copy, Debug)] +pub struct Reset { + pub ack: Option, + pub then: Option<(u64, u8)>, +} + +impl Reset { + pub const fn answered(ack: u64, then: u64) -> Self { + Self { ack: Some(ack), then: Some((then, 0xAA)) } + } +} + +pub struct Kbd { + /// The scan code set it sends in. + pub set: u8, + pub scanning: bool, + pub reset: Reset, + /// Its next byte loads at least this long after the read that freed the + /// buffer: the EC's queue. + pub gap: u64, + queue: VecDeque, + /// `a` pressed at each time. + typed: VecDeque, +} + +pub struct Aux { + pub reset: Option, + /// The id after its `0xAA`. + pub id: u8, + /// Its next byte loads this long after the read that freed the buffer: a + /// device held off while the buffer is full (TP 50). + pub gap: u64, + /// What each enable is answered, oldest first; `0xFA` once these run out. + pub enables: VecDeque>, + reporting: bool, + queue: VecDeque, + /// Packets, and self-resets, sent at each time. + sends: VecDeque<(u64, Vec)>, +} + +#[derive(Clone, Copy)] +enum Arg { + CommandByte, + Aux, +} + +pub struct Fake { + pub now: u64, + pub cb: u8, + pub kbd: Kbd, + pub aux: Aux, + /// Nothing decodes the ports. + pub absent: bool, + /// The controller drops a `0x60`'s byte. + pub drops_command_byte: bool, + /// The output buffer is full again the moment it is read. + pub wedged: bool, + /// The keyboard's bytes are translated whatever bit 6 says. + pub translates_always: bool, + /// Added to every wait that an interrupt ends: a reader that runs late. + pub latency: u64, + /// Every controller command written, in order. + pub commands: Vec, + /// When each keyboard byte loaded, and when the keyboard interface was + /// last enabled. + pub kbd_loads: Vec<(u64, u8)>, + pub kbd_enabled_at: u64, + obf: Option, + shown: Option, + freed: u64, + ibf_until: u64, + arg: Option, + controller: Option<(u64, u8)>, + irqs: Vec, + /// The translation saw `0xF0`. + released: bool, +} + +impl Fake { + /// A controller as firmware hands it over, with `cb` its command byte. + pub fn new(cb: u8, kbd_reset: Reset, aux_reset: Option) -> Self { + Self { + now: 0, + cb, + kbd: Kbd { set: 2, scanning: true, reset: kbd_reset, gap: 0, queue: VecDeque::new(), typed: VecDeque::new() }, + aux: Aux { + reset: aux_reset, + id: 0, + gap: 0, + enables: VecDeque::new(), + reporting: false, + queue: VecDeque::new(), + sends: VecDeque::new(), + }, + absent: false, + drops_command_byte: false, + wedged: false, + translates_always: false, + latency: 0, + commands: Vec::new(), + kbd_loads: Vec::new(), + kbd_enabled_at: 0, + obf: None, + shown: None, + freed: 0, + ibf_until: 0, + arg: None, + controller: None, + irqs: Vec::new(), + released: false, + } + } + + /// QEMU's: the keyboard and a PS/2 mouse each answer a reset at once. + pub fn qemu() -> Self { + Self::new(0x47, Reset::answered(50 * US, 300 * US), Some(Reset::answered(50 * US, 300 * US))) + } + + /// The T14's, as M1 and M2 read it: the keyboard's `0xFA` 80 µs after its + /// reset and its `0xAA` 516 ms after that, its EC queueing keyboard bytes; + /// the TrackPoint's `0xFA` at 2.5 ms and `0xAA` 367 ms later, held off + /// while the buffer is full. + pub fn t14() -> Self { + let mut fake = Self::new(0x47, Reset::answered(80 * US, 516 * MS), Some(Reset::answered(2_500 * US, 367 * MS))); + fake.kbd.gap = 30 * US; + fake.aux.gap = MS; + fake + } + + /// The AMD bench's: the keyboard's `0xFA` 581 µs after its reset and its + /// `0xAA` 2.2 ms after that; no aux answer; its EC translating whatever + /// bit 6 says. + pub fn amd() -> Self { + let mut fake = Self::new(0x47, Reset::answered(581 * US, 2_200 * US), None); + fake.translates_always = true; + fake + } + + /// Press `a` at `at`. + pub fn type_a(&mut self, at: u64) { + self.kbd.typed.push_back(at); + } + + /// The keyboard sends `bytes`, each when the last has been read, from `at`. + pub fn kbd_sends(&mut self, at: u64, bytes: &[u8]) { + for &value in bytes { + self.kbd.queue.push_back(Queued { ready: at, value, answer: false }); + } + } + + /// The aux device sends `bytes`, a millisecond apart, from `at`. + pub fn aux_sends(&mut self, at: u64, bytes: &[u8]) { + self.aux.sends.push_back((at, bytes.to_vec())); + } + + fn kbd_enabled(&self) -> bool { + self.cb & 0x10 == 0 + } + + fn aux_enabled(&self) -> bool { + self.cb & 0x20 == 0 + } + + /// Run the machine up to `to`. + fn advance(&mut self, to: u64) { + while let Some(&at) = self.kbd.typed.front().filter(|&&at| at <= to) { + self.kbd.typed.pop_front(); + if self.kbd.scanning { + let make = if self.kbd.set == 2 { 0x1C } else { 0x1E }; + self.kbd.queue.push_back(Queued { ready: at, value: make, answer: false }); + } + } + while let Some((at, _)) = self.aux.sends.front().filter(|(at, _)| *at <= to) { + let at = *at; + let (_, bytes) = self.aux.sends.pop_front().expect("a send was just seen"); + if bytes == [0xAA, 0x00] { + self.aux.reporting = false; + } else if !self.aux.reporting { + continue; + } + for (i, &value) in bytes.iter().enumerate() { + self.aux.queue.push_back(Queued { ready: at + i as u64 * MS, value, answer: false }); + } + } + loop { + if self.wedged && self.obf.is_none() { + self.obf = Some(Loaded { value: 0x55, src: Src::Kbd, answer: false, raised: false, taken: false }); + } + if self.obf.is_some() { + return; + } + let controller = self.controller.map(|(ready, _)| ready); + let kbd = self.kbd.queue.front().filter(|_| self.kbd_enabled()).map(|q| q.ready.max(self.freed + self.kbd.gap)); + let aux = self.aux.queue.front().filter(|_| self.aux_enabled()).map(|q| q.ready.max(self.freed + self.aux.gap)); + let next = [controller, kbd, aux].into_iter().flatten().min(); + let Some(at) = next.filter(|&at| at <= to) else { return }; + let (src, value, answer) = if Some(at) == controller { + let (_, value) = self.controller.take().expect("a controller answer was just seen"); + (Src::Controller, value, false) + } else if Some(at) == kbd { + let q = self.kbd.queue.pop_front().expect("a keyboard byte was just seen"); + (Src::Kbd, q.value, q.answer) + } else { + let q = self.aux.queue.pop_front().expect("an aux byte was just seen"); + (Src::Aux, q.value, q.answer) + }; + let value = match src { + Src::Kbd if self.cb & 0x40 != 0 || self.translates_always => match self.translate(value) { + Some(value) => value, + None => continue, + }, + _ => value, + }; + if src == Src::Kbd { + self.kbd_loads.push((at, value)); + } + let raised = match src { + Src::Kbd | Src::Controller => self.cb & 0x01 != 0, + Src::Aux => self.cb & 0x02 != 0, + }; + if raised { + self.irqs.push(at); + } + self.obf = Some(Loaded { value, src, answer, raised, taken: false }); + } + } + + /// The controller's translation (C 5), for the bytes the shapes send: + /// `None` for the `0xF0` that makes the next byte a release. + fn translate(&mut self, value: u8) -> Option { + let released = std::mem::take(&mut self.released); + let translated = match value { + 0xF0 => { + self.released = true; + return None; + } + 0x00 | 0xAA | 0xE0 | 0xEE | 0xFA | 0xFC | 0xFE => return Some(value), + // Set 2's `a`, and set 2's `2`, which is set 1's `a` read as set 2. + 0x1C => 0x1E, + 0x1E => 0x03, + // Set 2's left Shift. + 0x12 => 0x2A, + other => panic!("fake: no translation of {other:#04x} is modelled"), + }; + Some(if released { translated | 0x80 } else { translated }) + } + + fn written(&mut self, what: &str) { + self.advance(self.now); + assert!(self.obf.is_none(), "C 14: {what} written with the output buffer full"); + assert!(self.now >= self.ibf_until, "C 14: {what} written with the input buffer full"); + self.ibf_until = self.now + TAKES; + } + + fn keyboard_command(&mut self, byte: u8) { + assert_eq!(byte, 0xFF, "fake: the keyboard was sent {byte:#04x}, and no shape models it"); + let Reset { ack, then } = self.kbd.reset; + self.kbd.queue.clear(); + self.kbd.scanning = false; + let Some(ack) = ack else { return }; + let acked = self.now + ack; + self.kbd.queue.push_back(Queued { ready: acked, value: 0xFA, answer: true }); + if let Some((after, value)) = then { + self.kbd.queue.push_back(Queued { ready: acked + after, value, answer: true }); + if value == 0xAA { + self.kbd.set = 2; + self.kbd.scanning = true; + } + } + } + + fn aux_command(&mut self, byte: u8) { + let at = self.now; + match byte { + 0xFF => { + self.aux.reporting = false; + self.aux.queue.clear(); + let Some(Reset { ack: Some(ack), then }) = self.aux.reset else { return }; + self.aux.queue.push_back(Queued { ready: at + ack, value: 0xFA, answer: true }); + if let Some((after, value)) = then { + self.aux.queue.push_back(Queued { ready: at + ack + after, value, answer: true }); + self.aux.queue.push_back(Queued { ready: at + ack + after + 100 * US, value: self.aux.id, answer: true }); + } + } + 0xF4 => { + if self.aux.reset.is_none() { + return; + } + match self.aux.enables.pop_front().unwrap_or(Some(0xFA)) { + Some(0xFA) => { + self.aux.reporting = true; + self.aux.queue.push_back(Queued { ready: at + 100 * US, value: 0xFA, answer: true }); + } + Some(other) => self.aux.queue.push_back(Queued { ready: at + 100 * US, value: other, answer: true }), + None => {} + } + } + other => panic!("fake: the aux device was sent {other:#04x}, and no shape models it"), + } + } +} + +impl Controller for Fake { + fn status(&mut self) -> u8 { + if self.absent { + return 0xFF; + } + self.advance(self.now); + let mut status = self.cb & 0x04; + if let Some(loaded) = self.obf { + status |= OBF; + if loaded.src == Src::Aux { + status |= AUXB; + } + self.shown = Some(self.now); + } + if self.now < self.ibf_until { + status |= IBF; + } + status + } + + fn data(&mut self) -> u8 { + let loaded = self.obf.take().expect("fake: a data read with the output buffer empty"); + let shown = self.shown.take().expect("fake: a data read no status read showed"); + assert!( + self.now >= shown + READ_DELAY_NS, + "C 14: a data read {} ns after the status read that showed it", + self.now - shown + ); + assert!(!loaded.answer || loaded.taken, "C 11: the answer {:#04x} was read before its interrupt", loaded.value); + self.freed = self.now; + loaded.value + } + + fn command(&mut self, byte: u8) { + self.written(&format!("the command {byte:#04x}")); + self.commands.push(byte); + match byte { + 0x20 => { + assert!(!self.kbd_enabled(), "C 12: 0x20 written while the keyboard interface is enabled"); + self.controller = Some((self.now + 20 * US, self.cb)); + } + 0x60 => self.arg = Some(Arg::CommandByte), + 0xA7 => self.cb |= 0x20, + 0xA8 => self.cb &= !0x20, + 0xAD => self.cb |= 0x10, + 0xAE => { + self.cb &= !0x10; + self.kbd_enabled_at = self.now; + } + 0xD4 => self.arg = Some(Arg::Aux), + 0xA9 | 0xC0 | 0xD0 | 0xD1 | 0xE0 | 0xF0..=0xFF => { + panic!("fake: the controller command {byte:#04x} is one C lists, and no shape models it") + } + other => panic!("C 9: the controller command {other:#04x} is reserved"), + } + } + + fn write(&mut self, byte: u8) { + self.written(&format!("the data byte {byte:#04x}")); + match self.arg.take() { + Some(Arg::CommandByte) if self.drops_command_byte => {} + Some(Arg::CommandByte) => { + if self.cb & 0x10 != 0 && byte & 0x10 == 0 { + self.kbd_enabled_at = self.now; + } + self.cb = byte; + } + Some(Arg::Aux) => self.aux_command(byte), + None => self.keyboard_command(byte), + } + } + + fn now(&mut self) -> u64 { + self.now += US; + self.now + } + + fn wait(&mut self, deadline: u64) -> Option { + loop { + self.advance(self.now); + if let Some(&first) = self.irqs.iter().min() { + self.irqs.clear(); + if let Some(loaded) = self.obf.as_mut().filter(|l| l.raised) { + loaded.taken = true; + } + self.now += self.latency; + return Some(Irq { first_nanos: first }); + } + let next = self.next_event().filter(|&at| at <= deadline); + match next { + Some(at) => self.now = self.now.max(at), + None if self.now >= deadline => return None, + None => self.now = deadline, + } + } + } +} + +impl Fake { + /// When the machine next changes on its own, with nothing written. + fn next_event(&self) -> Option { + if self.obf.is_some() { + return None; + } + let kbd = self.kbd.queue.front().filter(|_| self.kbd_enabled()).map(|q| q.ready.max(self.freed + self.kbd.gap)); + let aux = self.aux.queue.front().filter(|_| self.aux_enabled()).map(|q| q.ready.max(self.freed + self.aux.gap)); + let typed = self.kbd.typed.front().copied(); + let sends = self.aux.sends.front().map(|(at, _)| *at); + [self.controller.map(|(at, _)| at), kbd, aux, typed, sends].into_iter().flatten().min() + } +} diff --git a/system/ps2server/src/hw.rs b/system/ps2server/src/hw.rs new file mode 100644 index 00000000000..2c3f79410ef --- /dev/null +++ b/system/ps2server/src/hw.rs @@ -0,0 +1,90 @@ +//! The machine's controller: its two ports, opened to this process by its +//! `isa` claim, and the claim's interrupt records. + +use toyos::poller::{Poller, READABLE}; +use toyos::{AsHandle, Device}; +use toyos_abi::ioport; +use toyos_abi::pci::DeviceIrqRecord; +use toyos_abi::syscall::{self, SyscallError}; + +use crate::bus::{Controller, Irq}; + +const DATA: u16 = 0x60; +const STATUS: u16 = 0x64; + +pub struct Hw { + claim: Device, + poller: Poller, + /// A watch on the claim is registered and has not fired. + watching: bool, +} + +impl Hw { + /// Over `claim`, whose description is read here: that read binds the + /// row's ports to this process. + pub fn bind(claim: Device) -> Self { + let mut description = [0u8; 16]; + let n = claim.read(&mut description).expect("ps2server: the claim's first read is its description"); + assert_eq!(n, description.len(), "ps2server: a description of {n} bytes"); + Self { claim, poller: Poller::new(1), watching: false } + } + + /// The claim's record, if it holds one. + fn record(&self) -> Option { + let mut record = [0u8; DeviceIrqRecord::SIZE]; + match syscall::read_nonblock(self.claim.as_handle(), &mut record) { + Ok(n) => { + assert_eq!(n, DeviceIrqRecord::SIZE, "ps2server: a record of {n} bytes"); + let first = record[8..16].try_into().expect("a record's first time is eight bytes"); + Some(Irq { first_nanos: u64::from_ne_bytes(first) }) + } + Err(SyscallError::WouldBlock) => None, + Err(other) => panic!("ps2server: a bound claim's read answered {other:?}"), + } + } +} + +impl Controller for Hw { + fn status(&mut self) -> u8 { + ioport::in8(STATUS) + } + + fn data(&mut self) -> u8 { + ioport::in8(DATA) + } + + fn command(&mut self, byte: u8) { + ioport::out8(STATUS, byte); + } + + fn write(&mut self, byte: u8) { + ioport::out8(DATA, byte); + } + + fn now(&mut self) -> u64 { + toyos_abi::clock::nanos_since_boot() + } + + fn wait(&mut self, deadline: u64) -> Option { + loop { + let now = self.now(); + let past = now >= deadline; + if let Some(irq) = self.record() { + return Some(irq); + } + if past { + return None; + } + if !self.watching { + self.poller.watch(&self.claim, READABLE, 0); + self.watching = true; + } + let timeout = if deadline == u64::MAX { u64::MAX } else { deadline - now }; + let mut fired = false; + self.poller.wait(1, timeout, |_| fired = true); + if fired { + self.watching = false; + } + } + } +} diff --git a/system/ps2server/src/main.rs b/system/ps2server/src/main.rs new file mode 100644 index 00000000000..9eb0a762deb --- /dev/null +++ b/system/ps2server/src/main.rs @@ -0,0 +1,90 @@ +//! `/system/bin/ps2server`: the i8042, its keyboard and its pointer, served. +//! +//! The kernel keeps the `isa` claim on the controller's ports and lines and +//! nothing else: it records each interrupt on the claim, and speaks none of +//! the controller's protocol. This program [`probe`]s the controller, then +//! runs the [`stream`], and hands the owner of input each key and pointer +//! report over its connection to the `input` port (`toyos_input::wire`), as +//! one source. +//! +//! **Every start is the same start.** The probe resets the keyboard and the +//! pointer and reads nothing a predecessor or firmware left, so a restart +//! needs no memory of the process before it, and what was typed while none +//! ran is not delivered. +//! +//! **It exits 0 only when it has nothing to serve**: no `isa` claim (a machine +//! with no i8042 row), port 0x64 reading `0xFF`, or the owner of input gone. +//! Every other end is a failure, said by name, which its supervisor restarts. + +mod bus; +mod hw; +mod probe; +mod stream; + +#[cfg(test)] +mod fake; + +use toyos::endow::{self, EndowError}; +use toyos::ipc::Connection; +use toyos::Device; +use toyos_abi::syscall::IsaId; +use toyos_input::wire::{self, Record, POINTER_LEN}; + +use crate::bus::Controller; +use crate::probe::Probed; + +/// The i8042's data and command ports, and its keyboard and aux lines. +fn i8042() -> IsaId { + IsaId::new(&[0x60, 0x64], &[1, 12]).expect("the i8042's set is a set") +} + +fn main() { + let mut say = |line: String| println!("{line}"); + let Some(claim) = endow::isa::(i8042()) else { + say("ps2server: no i8042 claim — nothing to serve".to_string()); + std::process::exit(0); + }; + let mut owner = connect(); + let mut hw = hw::Hw::bind(claim); + let (aux, backlog) = match probe::probe(&mut hw, &mut say) { + Probed::Absent => std::process::exit(0), + Probed::Failed => std::process::exit(1), + Probed::Armed { aux, backlog, .. } => (aux, backlog), + }; + let (mut stream, records) = stream::Stream::new(&mut hw, aux, &backlog, &mut say); + deliver(&mut owner, &records); + loop { + let irq = hw.wait(stream.deadline()); + match stream.wake(&mut hw, irq, &mut say) { + Ok(records) => deliver(&mut owner, &records), + Err(stream::Wedged) => std::process::exit(1), + } + } +} + +/// The connection to the owner of input, or the exit an owner that is gone +/// asks for. +fn connect() -> Connection { + match endow::service(wire::PORT) { + Ok(conn) => conn, + Err(EndowError::ServerGone) => { + println!("ps2server: the owner of input is gone — nothing to serve"); + std::process::exit(0); + } + Err(other) => panic!("ps2server: the manifest gives this program `{}`, and it answered {other:?}", wire::PORT), + } +} + +/// Hand the owner `records`, one frame each; an owner that hung up is asked +/// for again, and the new connection is a new source to it. +fn deliver(owner: &mut Connection, records: &[Record]) { + for record in records { + let mut payload = [0u8; POINTER_LEN]; + let (msg_type, len) = record.frame(&mut payload); + if owner.send_bytes(msg_type, &payload[..len]).is_err() { + *owner = connect(); + println!("ps2server: the owner of input hung up; connected again, and what it held is released"); + return; + } + } +} diff --git a/system/ps2server/src/probe.rs b/system/ps2server/src/probe.rs new file mode 100644 index 00000000000..ce79221000a --- /dev/null +++ b/system/ps2server/src/probe.rs @@ -0,0 +1,515 @@ +//! The probe: one sequence, the same on a boot's first start and on a +//! restart, which reads nothing firmware or a predecessor left. +//! +//! **C** is IBM's 84F9735 "Keyboard and Auxiliary Device Controller" +//! section, **K** its "Keyboards" section and **K88** the May 1988 edition's, +//! each by its own page; **TP** is IBM's *TrackPoint System Version 4.0 +//! Engineering Specification*, the auxiliary device's protocol. +//! +//! 1. **Quiet:** `0xAD`, `0xA7` (C 12), and the output buffer drained. +//! 2. **Keyboard:** the command byte with the keyboard's interrupt alone, +//! translation on and both interfaces still disabled; `0xAE`; then IBM's +//! Reset, `0xFF` (K 13), answered `0xFA` and the completion code `0xAA`, +//! after which the keyboard is in set 2 and scans (K 11, K 13). Translated +//! (C 10), set 2 is set 1 on the wire, whatever set firmware or a +//! predecessor left. The `0xAA` is taken whenever it comes inside the +//! stage's ceiling: K 11's 300-500 ms window decides nothing here. +//! 3. **`0xAD`:** the keyboard holds what it would send (K88 39). +//! 4. **Aux:** the command byte with the aux interrupt alone and the keyboard +//! interface still disabled, `0xA8`; `0xD4 0xFF`, answered `0xFA 0xAA` +//! and its id (TP 9, 12), and `0xD4 0xF4` (TP 13). A device that does not +//! answer its reset is absent. +//! 5. **Arming:** the command byte whole, read back with `0x20` while the +//! keyboard interface is disabled, so the answer is the controller's; then +//! `0xAE`, and the keyboard sends what it held. +//! +//! Every command byte holds the System Flag (C 11) and states the keyboard +//! interface disabled (bit 4): one written with it clear would enable the +//! keyboard as `0xAE` does. A device's answers are waited for on its +//! interrupt, and a byte found waiting before a write is drained, by name, +//! until the keyboard scans, and from then on is a keystroke for the stream. + +use crate::bus::*; + +/// The controller-only steps' ceiling: the quiet stage, each `0xAD`, the +/// arming. +const CONTROLLER_NS: u64 = 250_000_000; +/// The keyboard stage's, inside which its reset's `0xAA` is taken. +pub const KEYBOARD_NS: u64 = 750_000_000; +/// The aux stage's: TP 9's 500 ms of diagnostics, and its acknowledgements. +const AUX_NS: u64 = 600_000_000; + +/// The most the quiet stage reads before it calls the output buffer wedged. +const DRAIN: usize = 32; + +/// The keyboard's Reset, its acknowledgement and its completion code (K 13). +const RESET: u8 = 0xFF; +const ACK: u8 = 0xFA; +const PASSED: u8 = 0xAA; +/// The auxiliary device's Enable (TP 13). +const ENABLE: u8 = 0xF4; + +/// The command bytes the stages write, each with the System Flag and the +/// keyboard interface disabled. +const KEYBOARD_STAGE: u8 = CB_KEYBOARD_IRQ | CB_SYSTEM_FLAG | CB_KEYBOARD_DISABLED | CB_AUX_DISABLED | CB_TRANSLATE; +const AUX_STAGE: u8 = CB_AUX_IRQ | CB_SYSTEM_FLAG | CB_KEYBOARD_DISABLED | CB_TRANSLATE; + +/// What the probe left. +#[derive(Debug, PartialEq, Eq)] +pub enum Probed { + /// Nothing decodes the controller's ports. + Absent, + /// The keyboard, the pointer or both scan. `backlog` is every byte read + /// since they did, in order, for the stream's decoders first. + Armed { keyboard: bool, aux: bool, backlog: Vec }, + /// Neither is served, and each was said by name. + Failed, +} + +/// Run the probe over `c`, saying what each stage found through `say`. +pub fn probe(c: &mut impl Controller, say: &mut dyn FnMut(String)) -> Probed { + if c.status() == FLOATING { + say(format!("ps2server: absent — port 0x64 reads {FLOATING:#04x}, nothing decodes it")); + return Probed::Absent; + } + let mut probe = Probe { c, say, drained: Vec::new(), backlog: Vec::new(), scanning: false, aux_reporting: false }; + match probe.run() { + Ok(probed) => probed, + Err(Stopped) => Probed::Failed, + } +} + +/// A stage said why the probe ends here. +struct Stopped; + +struct Probe<'a, C: Controller> { + c: &'a mut C, + say: &'a mut dyn FnMut(String), + /// Bytes read before they could be anybody's input, not yet said. + drained: Vec, + backlog: Vec, + /// The keyboard's reset completed: what it sends now is typed. + scanning: bool, + /// The aux device reports: what it sends now is motion. + aux_reporting: bool, +} + +impl Probe<'_, C> { + fn run(&mut self) -> Result { + self.quiet()?; + let keyboard = self.keyboard()?; + let deadline = self.deadline(CONTROLLER_NS); + self.command(DISABLE_KEYBOARD, deadline, "0xAD after the keyboard stage")?; + let aux = self.aux()?; + if !keyboard && !aux { + (self.say)("ps2server: neither the keyboard nor a pointer to serve".to_string()); + return Err(Stopped); + } + self.arm(keyboard, aux)?; + Ok(Probed::Armed { keyboard, aux, backlog: core::mem::take(&mut self.backlog) }) + } + + fn deadline(&mut self, ns: u64) -> u64 { + self.c.now() + ns + } + + /// Where a byte read off the output buffer goes, by when it was read. + fn route(&mut self, byte: Byte) { + let input = if byte.aux { self.aux_reporting } else { self.scanning }; + if input { + self.backlog.push(byte); + } else { + self.drained.push(byte); + } + } + + /// Say what was drained since the last time, by name. + fn said_drained(&mut self, when: &str) { + if self.drained.is_empty() { + return; + } + let bytes: Vec = self + .drained + .drain(..) + .map(|b| if b.aux { format!("aux {:#04x}", b.value) } else { format!("{:#04x}", b.value) }) + .collect(); + (self.say)(format!("ps2server: drained {} byte(s) {when}: [{}]", bytes.len(), bytes.join(", "))); + } + + fn send(&mut self, port: Port, byte: u8, deadline: u64, what: &str) -> Result<(), Stopped> { + let mut found = Vec::new(); + let sent = self.c.send(port, byte, deadline, &mut |b| found.push(b)); + for b in found { + self.route(b); + } + sent.map_err(|NotTaken { status }| { + (self.say)(format!("ps2server: the controller never took {what} (status {status:#04x})")); + Stopped + }) + } + + fn command(&mut self, byte: u8, deadline: u64, what: &str) -> Result<(), Stopped> { + self.send(Port::Command, byte, deadline, what) + } + + fn command_byte(&mut self, byte: u8, deadline: u64) -> Result<(), Stopped> { + self.command(WRITE_COMMAND_BYTE, deadline, "0x60")?; + self.send(Port::Data, byte, deadline, &format!("the command byte {byte:#04x}")) + } + + /// The next byte of the device `aux` names, waited for on its interrupt + /// until `deadline`; every other byte read on the way is routed. + fn answer(&mut self, aux: bool, deadline: u64) -> Result { + loop { + let irq = self.c.wait(deadline); + while let Some(byte) = self.c.poll() { + if byte.aux == aux { + return Ok(byte.value); + } + self.route(byte); + } + if irq.is_none() { + let status = self.c.status(); + return Err(Silent { full: status & OBF != 0 }); + } + } + } + + /// Step 1. + fn quiet(&mut self) -> Result<(), Stopped> { + let deadline = self.deadline(CONTROLLER_NS); + self.command(DISABLE_KEYBOARD, deadline, "0xAD")?; + self.command(DISABLE_AUX, deadline, "0xA7")?; + for _ in 0..DRAIN { + match self.c.poll() { + Some(byte) => self.route(byte), + None => { + self.said_drained("before the probe"); + return Ok(()); + } + } + } + self.said_drained("before the probe"); + (self.say)(format!("ps2server: the output buffer never drains — {DRAIN} reads left it full")); + Err(Stopped) + } + + /// Step 2: whether the keyboard scans, set 2 translated. + fn keyboard(&mut self) -> Result { + let deadline = self.deadline(KEYBOARD_NS); + self.command_byte(KEYBOARD_STAGE, deadline)?; + self.command(ENABLE_KEYBOARD, deadline, "0xAE")?; + self.send(Port::Data, RESET, deadline, "the keyboard's reset")?; + let reset = self.c.now(); + let disabled = |this: &mut Self, why: String| { + this.said_drained("before the keyboard scanned"); + (this.say)(format!("ps2server: kbd DISABLED — {why}")); + Ok(false) + }; + match self.answer(false, deadline) { + Ok(ACK) => {} + Ok(other) => return disabled(self, format!("the reset answered {other:#04x}, not 0xfa")), + Err(silent) => return disabled(self, format!("no 0xfa to the reset in {} ms ({silent})", KEYBOARD_NS / 1_000_000)), + } + let acked = self.c.now(); + match self.answer(false, deadline) { + Ok(PASSED) => {} + Ok(other) => return disabled(self, format!("the reset's 0xfa was followed by {other:#04x}, not 0xaa")), + Err(silent) => return disabled(self, format!("no 0xaa in {} ms of the reset ({silent})", KEYBOARD_NS / 1_000_000)), + } + let passed = self.c.now(); + self.scanning = true; + self.said_drained("before the keyboard scanned"); + (self.say)(format!( + "ps2server: kbd reset, 0xfa {} us after the command and 0xaa {} us after the 0xfa — set 2, translated", + (acked - reset) / 1_000, + (passed - acked) / 1_000 + )); + Ok(true) + } + + /// Step 4: whether the aux device reports. + fn aux(&mut self) -> Result { + let deadline = self.deadline(AUX_NS); + self.command_byte(AUX_STAGE, deadline)?; + self.command(ENABLE_AUX, deadline, "0xA8")?; + self.command(WRITE_AUX, deadline, "0xD4")?; + self.send(Port::Data, RESET, deadline, "the aux device's reset")?; + match self.answer(true, deadline) { + Ok(ACK) => {} + Ok(other) => return self.aux_off(format!("aux DISABLED — the reset answered {other:#04x}, not 0xfa")), + Err(silent) => { + let ms = AUX_NS / 1_000_000; + return self.aux_off(format!("aux absent — its reset went unanswered in {ms} ms ({silent})")); + } + } + let acked = self.c.now(); + let completion = self.answer(true, deadline); + let passed = self.c.now(); + let id = match (&completion, self.answer(true, deadline)) { + (Ok(PASSED), Ok(id)) => id, + (_, id) => { + return self.aux_off(format!( + "aux DISABLED — the reset's 0xfa was followed by {completion:?} and {id:?}, not 0xaa and an id" + )) + } + }; + self.command(WRITE_AUX, deadline, "0xD4")?; + self.send(Port::Data, ENABLE, deadline, "the aux device's enable")?; + // From the enable on, what the device sends is motion. + self.aux_reporting = true; + match self.answer(true, deadline) { + Ok(ACK) => {} + other => { + self.aux_reporting = false; + return self.aux_off(format!("aux DISABLED — its enable answered {other:?}, not 0xfa")); + } + } + self.said_drained("before the aux device reported"); + (self.say)(format!( + "ps2server: aux reset, 0xaa {} us after its 0xfa, id {id:#04x} — reporting", + (passed - acked) / 1_000 + )); + Ok(true) + } + + /// The aux device is not served, and `why` is said. + fn aux_off(&mut self, why: String) -> Result { + self.said_drained("before the aux device reported"); + (self.say)(format!("ps2server: {why}")); + Ok(false) + } + + /// Step 5. + fn arm(&mut self, keyboard: bool, aux: bool) -> Result<(), Stopped> { + let deadline = self.deadline(CONTROLLER_NS); + let mut byte = CB_SYSTEM_FLAG | CB_KEYBOARD_DISABLED | CB_TRANSLATE; + if keyboard { + byte |= CB_KEYBOARD_IRQ; + } + byte |= if aux { CB_AUX_IRQ } else { CB_AUX_DISABLED }; + self.command_byte(byte, deadline)?; + self.command(READ_COMMAND_BYTE, deadline, "0x20")?; + let read = loop { + let past = self.c.now() >= deadline; + match self.c.poll() { + Some(Byte { value, aux: false }) => break Some(value), + Some(motion) => self.route(motion), + None if past => break None, + None => core::hint::spin_loop(), + } + }; + if read != Some(byte) { + let read = read.map_or("nothing".to_string(), |v| format!("{v:#04x}")); + (self.say)(format!("ps2server: DISABLED — command byte {byte:#04x} did not take (read back {read})")); + return Err(Stopped); + } + if keyboard { + self.command(ENABLE_KEYBOARD, deadline, "0xAE")?; + } + (self.say)(format!( + "ps2server: armed, command byte {byte:#04x} — keyboard {}, aux {}", + if keyboard { "on" } else { "off" }, + if aux { "on" } else { "off" } + )); + Ok(()) + } +} + +/// No answer by the deadline; whether the output buffer held a byte anyway. +#[derive(Debug)] +struct Silent { + full: bool, +} + +impl core::fmt::Display for Silent { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(if self.full { "the output buffer full" } else { "the output buffer empty" }) + } +} + +#[cfg(test)] +mod tests { + use toyos_input::wire::Record; + + use super::*; + use crate::fake::{Fake, Reset}; + use crate::stream::Stream; + + const MS: u64 = 1_000_000; + const US: u64 = 1_000; + + fn probed(fake: &mut Fake) -> (Probed, Vec) { + let mut said = Vec::new(); + let probed = probe(fake, &mut |line| said.push(line)); + (probed, said) + } + + fn armed(fake: &mut Fake) -> (bool, bool, Vec, Vec) { + match probed(fake) { + (Probed::Armed { keyboard, aux, backlog }, said) => (keyboard, aux, backlog, said), + (other, said) => panic!("probed {other:?}: {said:#?}"), + } + } + + /// Every record the stream makes until `until`, from what the probe left. + fn records(fake: &mut Fake, aux: bool, backlog: &[Byte], until: u64) -> Vec { + let mut said = |line: String| eprintln!("{line}"); + let (mut stream, mut out) = Stream::new(fake, aux, backlog, &mut said); + loop { + let irq = fake.wait(stream.deadline().min(until)); + if irq.is_none() && fake.now >= until { + return out; + } + out.extend(stream.wake(fake, irq, &mut said).expect("the stream ended")); + } + } + + fn typed_a(records: &[Record]) -> bool { + records.iter().any(|r| matches!(r, Record::Key(k) if k.keycode == 0x04 && k.pressed())) + } + + fn line<'a>(said: &'a [String], start: &str) -> &'a str { + said.iter().find(|l| l.starts_with(start)).unwrap_or_else(|| panic!("no {start:?} in {said:#?}")) + } + + /// The two times the keyboard's line names, in microseconds. + fn reset_times(said: &[String]) -> (u64, u64) { + let words: Vec<&str> = line(said, "ps2server: kbd reset").split(' ').collect(); + let at = |after: &str| -> u64 { + let i = words.iter().position(|w| *w == after).expect("the line's shape"); + words[i + 1].parse().expect("a time") + }; + (at("0xfa"), at("0xaa")) + } + + #[test] + fn qemu_arms_its_keyboard_set_2_translated_and_its_mouse() { + let mut fake = Fake::qemu(); + let (keyboard, aux, backlog, said) = armed(&mut fake); + assert!(keyboard && aux, "{said:#?}"); + assert_eq!(fake.cb, 0x47, "both interrupts, the System Flag, translation, both clocks on"); + line(&said, "ps2server: armed, command byte 0x57 — keyboard on, aux on"); + line(&said, "ps2server: aux reset"); + let at = fake.now + MS; + fake.type_a(at); + assert!(typed_a(&records(&mut fake, aux, &backlog, at + 10 * MS))); + } + + #[test] + fn the_t14_arms_its_keyboard_and_its_trackpoint() { + let mut fake = Fake::t14(); + let (keyboard, aux, _, said) = armed(&mut fake); + assert!(keyboard && aux, "{said:#?}"); + let (acked, passed) = reset_times(&said); + assert!((80..100).contains(&acked), "the 0xfa {acked} us after the reset"); + assert!((516_000..516_100).contains(&passed), "the 0xaa {passed} us after the 0xfa"); + assert!(line(&said, "ps2server: aux reset").contains("0xaa 367"), "{said:#?}"); + assert_eq!(fake.cb, 0x47); + } + + #[test] + fn the_amd_bench_arms_its_keyboard_and_finds_no_pointer() { + let mut fake = Fake::amd(); + let (keyboard, aux, backlog, said) = armed(&mut fake); + assert!(keyboard && !aux, "{said:#?}"); + let (_, passed) = reset_times(&said); + assert!((2_200..2_300).contains(&passed), "the 0xaa {passed} us after the 0xfa"); + line(&said, "ps2server: aux absent — its reset went unanswered in 600 ms"); + assert_eq!(fake.cb, 0x65, "the keyboard's interrupt, the System Flag, translation, the aux clock off"); + line(&said, "ps2server: armed, command byte 0x75 — keyboard on, aux off"); + let at = fake.now + MS; + fake.type_a(at); + assert!(typed_a(&records(&mut fake, aux, &backlog, at + 10 * MS))); + } + + /// A key pressed while the TrackPoint runs its reset: the keyboard holds + /// it until the arming's `0xAE`, so it is never drained, never read as the + /// command byte and never holds the output buffer against the aux answer. + #[test] + fn a_keystroke_during_the_aux_reset_reaches_the_keyboard_after_the_arming() { + let mut fake = Fake::t14(); + // The keyboard stage ends 516 ms in; the aux device answers its reset + // 369.5 ms after that. + fake.type_a(700 * MS); + let (keyboard, aux, backlog, said) = armed(&mut fake); + assert!(keyboard && aux, "{said:#?}"); + assert!(!said.iter().any(|l| l.contains("drained")), "{said:#?}"); + let until = fake.now + 10 * MS; + let records = records(&mut fake, aux, &backlog, until); + assert!(typed_a(&records), "{records:?}"); + let loaded = fake.kbd_loads.iter().find(|(_, v)| *v == 0x1E).expect("the keystroke loaded"); + assert!(loaded.0 >= fake.kbd_enabled_at, "it loaded at {} ns, the arming's 0xAE at {}", loaded.0, fake.kbd_enabled_at); + } + + /// K 1: a keyboard POST left in set 1, behind a controller that does not + /// translate. The stage translates before the reset, which leaves set 2. + #[test] + fn a_start_after_set_1_with_translation_off_arms_set_2_translated() { + let mut fake = Fake::t14(); + fake.cb = 0x07; + fake.kbd.set = 1; + let (keyboard, aux, backlog, said) = armed(&mut fake); + assert!(keyboard && aux, "{said:#?}"); + let at = fake.now + MS; + fake.type_a(at); + assert!(typed_a(&records(&mut fake, aux, &backlog, at + 10 * MS))); + } + + #[test] + fn a_reset_answered_0xfc_disables_the_keyboard_by_name() { + let mut fake = Fake::t14(); + fake.kbd.reset = Reset { ack: Some(80 * US), then: Some((516 * MS, 0xFC)) }; + let (keyboard, aux, _, said) = armed(&mut fake); + assert!(!keyboard && aux, "{said:#?}"); + line(&said, "ps2server: kbd DISABLED — the reset's 0xfa was followed by 0xfc, not 0xaa"); + assert_eq!(fake.cb, 0x56, "the keyboard's interrupt off and its interface disabled"); + } + + #[test] + fn an_0xaa_past_the_keyboard_stages_ceiling_disables_the_keyboard_by_name() { + let mut fake = Fake::t14(); + fake.kbd.reset = Reset::answered(80 * US, KEYBOARD_NS + MS); + let (keyboard, _, _, said) = armed(&mut fake); + assert!(!keyboard, "{said:#?}"); + line(&said, "ps2server: kbd DISABLED — no 0xaa in 750 ms of the reset (the output buffer empty)"); + } + + #[test] + fn a_reset_with_no_0xfa_disables_the_keyboard_by_name() { + let mut fake = Fake::t14(); + fake.kbd.reset = Reset { ack: None, then: None }; + let (keyboard, _, _, said) = armed(&mut fake); + assert!(!keyboard, "{said:#?}"); + line(&said, "ps2server: kbd DISABLED — no 0xfa to the reset in 750 ms (the output buffer empty)"); + } + + #[test] + fn a_floating_status_port_is_no_controller() { + let mut fake = Fake::t14(); + fake.absent = true; + let (probed, said) = probed(&mut fake); + assert_eq!(probed, Probed::Absent); + line(&said, "ps2server: absent — port 0x64 reads 0xff"); + assert!(fake.commands.is_empty(), "a byte written to ports nothing decodes: {:02x?}", fake.commands); + } + + #[test] + fn a_command_byte_the_controller_dropped_is_said_by_name() { + let mut fake = Fake::t14(); + fake.cb = 0x07; + fake.drops_command_byte = true; + let (probed, said) = probed(&mut fake); + assert_eq!(probed, Probed::Failed); + line(&said, "ps2server: DISABLED — command byte 0x57 did not take (read back 0x17)"); + } + + #[test] + fn bytes_waiting_before_the_probe_are_drained_by_name() { + let mut fake = Fake::t14(); + fake.kbd_sends(0, &[0x1C, 0x1C]); + let (_, _, backlog, said) = armed(&mut fake); + assert!(backlog.is_empty(), "{backlog:?}"); + assert!(said.iter().any(|l| l.starts_with("ps2server: drained") && l.contains("0x1e")), "{said:#?}"); + } +} diff --git a/system/ps2server/src/stream.rs b/system/ps2server/src/stream.rs new file mode 100644 index 00000000000..4fa975e68ff --- /dev/null +++ b/system/ps2server/src/stream.rs @@ -0,0 +1,440 @@ +//! The stream: one wake per interrupt record, which empties the output +//! buffer into the decoders and hands the owner of input what they made. +//! +//! **One wake** takes the claim's record; while the output buffer is full, +//! up to [`BURST`] bytes, reads one byte and feeds the decoder `AUXB` names; +//! and reads the clock after the status read that found it empty, `t_end`. +//! Both lines are edge-triggered and the controller raises its line whenever +//! it loads a byte (C 11), so a byte that loads after that status read raises +//! its own wake. Since the output buffer empties when it is read (C 7), one +//! still full after [`BURST`] reads is no state IBM's protocol makes: the +//! stream ends by name, and its supervisor's restart limit decides the rest. +//! +//! **The pointer's framer is fed the device's own idle time.** A wake's first +//! aux byte is fed the gap from the last wake's `t_end` to its record's first +//! interrupt, every later byte of the wake none. A device held off while the +//! output buffer is full (TP 50) loads its next byte one byte's time after +//! the read that released it, which is before `t_end`, and one that queues +//! loads it in the same wake: a slow reader delays a resync and never splits a +//! packet. +//! +//! **A device that resets itself** (`0xAA 0x00`) has its reporting enabled +//! again with `0xD4 0xF4` (TP 13), whose `0xFA` comes in the stream within +//! TP 46's 25 ms; [`REENABLES`] failures in a row write the pointer off with +//! `0xA7` alone (C 12), so its line never rises again. + +use toyos_abi::input::{RawKeyEvent, MOD_RELEASED}; +use toyos_input::wire::{Motion, Record, Report}; +use toyos_ps2::{KeyDecoder, KeyOutcome, MouseDecoder, MouseOutcome}; + +use crate::bus::*; + +/// The most bytes one wake reads before it calls the controller wedged. +pub const BURST: usize = 32; + +/// TP 46: the device acknowledges a command within 25 ms. +const ACK_NS: u64 = 25_000_000; +/// Re-enables in a row that fail before the pointer is written off. +const REENABLES: u32 = 3; +/// The device's Enable, and its acknowledgement. +const ENABLE: u8 = 0xF4; +const ACK: u8 = 0xFA; + +/// How often the counters are said, at most, and only after an interrupt. +const COUNTERS_NS: u64 = 10_000_000_000; +/// The undecoded runs said one by one; past it they are counted. +const NAMED_RUNS: u32 = 8; +/// The most bytes of one undecoded run named. +const RUN: usize = 8; + +/// The output buffer stayed full for [`BURST`] reads, said with what they +/// read. +#[derive(Debug)] +pub struct Wedged; + +/// What the pointer's line is doing. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Aux { + /// Not served: absent, or written off. + Off, + Reporting, + /// It reset itself; an enable is owed once the buffers are empty. + Owed { failures: u32 }, + /// The enable is out, and its acknowledgement due by `by`. + Enabling { by: u64, failures: u32 }, +} + +/// Bytes a decoder has taken and not yet resolved, kept until the byte that +/// ends the run, so an undecodable sequence is named whole. +#[derive(Default)] +struct Run(Vec); + +impl Run { + fn push(&mut self, byte: u8) { + if self.0.len() < RUN { + self.0.push(byte); + } + } + + /// The run ended in `last` and made nothing: all of it, by name. + fn blame(&mut self, last: u8, aux: bool) -> String { + self.push(last); + let bytes: Vec = self.0.drain(..).map(|b| format!("{b:#04x}")).collect(); + format!("{}[{}]", if aux { "aux " } else { "" }, bytes.join(", ")) + } +} + +#[derive(Default)] +struct Counts { + irqs: u64, + bytes: u64, + keys: u64, + motion: u64, + undecoded: u32, + discarded: u64, + overruns: u64, +} + +pub struct Stream { + keys: KeyDecoder, + pointer: MouseDecoder, + kbd_run: Run, + aux_run: Run, + aux: Aux, + /// The running sum of the device's own idle gaps: the framer's clock. + device_ns: u64, + /// The last wake's `t_end`. + end: u64, + counts: Counts, + /// Interrupts as of the last counters line, and when the next may be said. + said_irqs: u64, + next_counters: u64, +} + +impl Stream { + /// The stream after the probe, with the pointer served where `aux`, and + /// the probe's `backlog` decoded first. + pub fn new(c: &mut impl Controller, aux: bool, backlog: &[Byte], say: &mut dyn FnMut(String)) -> (Self, Vec) { + let now = c.now(); + let mut stream = Self { + keys: KeyDecoder::new(), + pointer: MouseDecoder::new(), + kbd_run: Run::default(), + aux_run: Run::default(), + aux: if aux { Aux::Reporting } else { Aux::Off }, + device_ns: 0, + end: now, + counts: Counts::default(), + said_irqs: 0, + next_counters: now + COUNTERS_NS, + }; + let mut out = Vec::new(); + for &byte in backlog { + stream.feed(byte, &mut out, say); + } + (stream, out) + } + + /// When the stream next has to run whether or not an interrupt comes. + pub fn deadline(&self) -> u64 { + match self.aux { + Aux::Enabling { by, .. } => by, + Aux::Off | Aux::Reporting | Aux::Owed { .. } => u64::MAX, + } + } + + /// One wake: `irq` is the record it took, or `None` where + /// [`Self::deadline`] passed with none. + pub fn wake(&mut self, c: &mut impl Controller, irq: Option, say: &mut dyn FnMut(String)) -> Result, Wedged> { + let mut out = Vec::new(); + if irq.is_some() { + self.counts.irqs += 1; + } + let mut gap = irq.map(|irq| irq.first_nanos.saturating_sub(self.end)); + let mut last = Vec::new(); + loop { + let status = c.status(); + let shown = c.now(); + if status & OBF == 0 { + self.end = shown; + break; + } + if last.len() == BURST { + say(format!("ps2server: the output buffer stayed full for {BURST} reads, the last {last:02x?} — the controller is wedged")); + return Err(Wedged); + } + let byte = Byte { value: c.read_after(shown), aux: status & AUXB != 0 }; + last.push(byte.value); + if byte.aux { + self.device_ns += gap.take().unwrap_or(0); + } + self.feed(byte, &mut out, say); + } + if let Aux::Enabling { by, failures } = self.aux { + if self.end >= by { + say(format!("ps2server: aux reset itself, and its enable went unanswered in {} ms", ACK_NS / 1_000_000)); + self.aux = Aux::Owed { failures: failures + 1 }; + } + } + if let Aux::Owed { failures } = self.aux { + self.reenable(c, failures, &mut out, say); + } + self.counters(say); + Ok(out) + } + + /// One byte to the decoder `AUXB` named. + fn feed(&mut self, byte: Byte, out: &mut Vec, say: &mut dyn FnMut(String)) { + self.counts.bytes += 1; + if byte.aux { + self.aux_byte(byte.value, out, say); + } else { + self.key_byte(byte.value, out, say); + } + } + + fn key_byte(&mut self, byte: u8, out: &mut Vec, say: &mut dyn FnMut(String)) { + match self.keys.feed(byte) { + KeyOutcome::Pending => return self.kbd_run.push(byte), + KeyOutcome::Key { usage, pressed } => { + self.counts.keys += 1; + let modifiers = if pressed { 0 } else { MOD_RELEASED }; + out.push(Record::Key(RawKeyEvent { keycode: usage, modifiers })); + } + // The keyboard's overrun code: what it lost may be a release. + KeyOutcome::Lost => { + self.counts.overruns += 1; + out.push(Record::Lost); + } + KeyOutcome::None => { + let run = self.kbd_run.blame(byte, false); + return self.undecoded(run, say); + } + } + self.kbd_run.0.clear(); + } + + fn aux_byte(&mut self, byte: u8, out: &mut Vec, say: &mut dyn FnMut(String)) { + match self.aux { + Aux::Reporting => {} + Aux::Enabling { failures, .. } => { + if byte == ACK { + say("ps2server: aux reset itself, reporting enabled again".to_string()); + self.aux = Aux::Reporting; + } else { + say(format!("ps2server: aux reset itself, and its enable answered {byte:#04x}, not 0xfa")); + self.aux = Aux::Owed { failures: failures + 1 }; + } + return; + } + // Nothing is reported until the enable is out, and nothing after + // the write-off: a byte now is one this stream had to drop. + Aux::Off | Aux::Owed { .. } => { + self.counts.discarded += 1; + return out.push(Record::Lost); + } + } + match self.pointer.feed(byte, self.device_ns) { + MouseOutcome::Pending => return self.aux_run.push(byte), + MouseOutcome::Packet { buttons, dx, dy } => { + self.counts.motion += 1; + // A 9-bit count, which an `i16` holds. + let motion = Motion::Relative { dx: dx as i16, dy: dy as i16 }; + out.push(Record::Pointer(Report { source: 0, buttons, scroll: 0, motion })); + } + MouseOutcome::Reset => self.aux = Aux::Owed { failures: 0 }, + MouseOutcome::Discarded => { + self.counts.discarded += 1; + out.push(Record::Lost); + } + } + self.aux_run.0.clear(); + } + + fn undecoded(&mut self, run: String, say: &mut dyn FnMut(String)) { + self.counts.undecoded += 1; + if self.counts.undecoded <= NAMED_RUNS { + say(format!("ps2server: no event from {run}")); + } + } + + /// Send the aux device its enable, or write it off after + /// [`REENABLES`] failures. + fn reenable(&mut self, c: &mut impl Controller, failures: u32, out: &mut Vec, say: &mut dyn FnMut(String)) { + let deadline = c.now() + ACK_NS; + if failures >= REENABLES { + self.aux = Aux::Off; + out.push(Record::Lost); + let sent = self.send(c, Port::Command, DISABLE_AUX, deadline, out, say); + say(format!("ps2server: aux enable failed {failures} times — pointer written off ({sent:?})")); + return; + } + let sent = self + .send(c, Port::Command, WRITE_AUX, deadline, out, say) + .and_then(|()| self.send(c, Port::Data, ENABLE, deadline, out, say)); + match sent { + Ok(()) => self.aux = Aux::Enabling { by: c.now() + ACK_NS, failures }, + Err(NotTaken { status }) => { + say(format!("ps2server: aux reset itself, and the controller never took its enable (status {status:#04x})")); + self.aux = Aux::Owed { failures: failures + 1 }; + } + } + } + + /// A write, feeding what was waiting before it to the decoders. + fn send( + &mut self, + c: &mut impl Controller, + port: Port, + byte: u8, + deadline: u64, + out: &mut Vec, + say: &mut dyn FnMut(String), + ) -> Result<(), NotTaken> { + let mut found = Vec::new(); + let sent = c.send(port, byte, deadline, &mut |b| found.push(b)); + for b in found { + self.feed(b, out, say); + } + sent + } + + fn counters(&mut self, say: &mut dyn FnMut(String)) { + if self.counts.irqs == self.said_irqs || self.end < self.next_counters { + return; + } + self.said_irqs = self.counts.irqs; + self.next_counters = self.end + COUNTERS_NS; + let Counts { irqs, bytes, keys, motion, undecoded, discarded, overruns } = self.counts; + say(format!( + "ps2server: {irqs} interrupts, {bytes} bytes, {keys} keys, {motion} motion, {undecoded} undecoded, \ + {discarded} discarded, {overruns} overruns" + )); + } +} + +#[cfg(test)] +mod tests { + use toyos_abi::input::MOD_RELEASED; + use toyos_input::wire::{Motion, Record, Report}; + + use super::*; + use crate::fake::Fake; + use crate::probe::{probe, Probed}; + + const MS: u64 = 1_000_000; + + /// `fake` probed and armed, and the stream over it. + fn armed(fake: &mut Fake) -> Stream { + let mut said = |line: String| eprintln!("{line}"); + let Probed::Armed { aux, backlog, .. } = probe(fake, &mut said) else { panic!("the probe armed nothing") }; + let (stream, records) = Stream::new(fake, aux, &backlog, &mut said); + assert!(records.is_empty(), "{records:?}"); + stream + } + + /// Every record and line the stream makes until `until`. + fn run(fake: &mut Fake, stream: &mut Stream, until: u64) -> (Vec, Vec) { + let (mut out, mut said) = (Vec::new(), Vec::new()); + loop { + let irq = fake.wait(stream.deadline().min(until)); + if irq.is_none() && fake.now >= until { + return (out, said); + } + out.extend(stream.wake(fake, irq, &mut |line| said.push(line)).expect("the stream ended")); + } + } + + #[test] + fn a_keystroke_is_its_make_and_its_break() { + let mut fake = Fake::qemu(); + let mut stream = armed(&mut fake); + let at = fake.now + MS; + fake.kbd_sends(at, &[0x1C, 0xF0, 0x1C]); + let (records, _) = run(&mut fake, &mut stream, at + 10 * MS); + let made = toyos_abi::input::RawKeyEvent { keycode: 0x04, modifiers: 0 }; + let broke = toyos_abi::input::RawKeyEvent { keycode: 0x04, modifiers: MOD_RELEASED }; + assert_eq!(records, [Record::Key(made), Record::Key(broke)]); + } + + #[test] + fn an_overrun_code_sends_lost() { + let mut fake = Fake::qemu(); + let mut stream = armed(&mut fake); + let at = fake.now + MS; + fake.kbd_sends(at, &[0x00]); + let (records, _) = run(&mut fake, &mut stream, at + 10 * MS); + assert_eq!(records, [Record::Lost]); + } + + /// `E0 2A` is the fake Shift around PrtScn, which maps to nothing: the run + /// is named whole. + #[test] + fn an_undecoded_run_is_named_whole() { + let mut fake = Fake::qemu(); + let mut stream = armed(&mut fake); + let at = fake.now + MS; + fake.kbd_sends(at, &[0xE0, 0x12]); + let (records, said) = run(&mut fake, &mut stream, at + 10 * MS); + assert!(records.is_empty(), "{records:?}"); + assert!(said.iter().any(|l| l == "ps2server: no event from [0xe0, 0x2a]"), "{said:#?}"); + } + + #[test] + fn an_output_buffer_full_after_32_reads_ends_the_stream_by_name() { + let mut fake = Fake::qemu(); + let mut stream = armed(&mut fake); + fake.wedged = true; + let mut said = Vec::new(); + let ended = stream.wake(&mut fake, None, &mut |line| said.push(line)); + assert!(ended.is_err(), "{ended:?}"); + assert!(said.iter().any(|l| l.contains("stayed full for 32 reads")), "{said:#?}"); + } + + /// The device resets itself, and a keystroke loads between the wake's + /// last read and the enable's second byte: it reaches the keyboard's + /// decoder, and the enable goes out after it. + #[test] + fn a_keystroke_found_waiting_before_the_reenable_reaches_the_keyboards_decoder() { + let mut fake = Fake::qemu(); + let mut stream = armed(&mut fake); + fake.kbd.gap = 3_000; + let at = fake.now + MS; + fake.aux_sends(at, &[0xAA, 0x00]); + fake.kbd_sends(at + MS, &[0x1C, 0x1C]); + let (records, said) = run(&mut fake, &mut stream, at + 30 * MS); + let keys = records.iter().filter(|r| matches!(r, Record::Key(k) if k.keycode == 0x04)).count(); + assert_eq!(keys, 2, "{records:?}"); + assert!(said.iter().any(|l| l == "ps2server: aux reset itself, reporting enabled again"), "{said:#?}"); + } + + #[test] + fn three_failed_reenables_write_the_pointer_off_with_0xa7() { + let mut fake = Fake::qemu(); + let mut stream = armed(&mut fake); + fake.aux.enables.extend([None, None, None]); + let at = fake.now + MS; + fake.aux_sends(at, &[0xAA, 0x00]); + let before = fake.commands.len(); + let (records, said) = run(&mut fake, &mut stream, at + 200 * MS); + assert_eq!(fake.commands[before..], [0xD4, 0xD4, 0xD4, 0xA7], "the enable thrice, then 0xA7 alone"); + assert_eq!(records, [Record::Lost], "the pointer's buttons released"); + assert!(said.iter().any(|l| l.starts_with("ps2server: aux enable failed 3 times — pointer written off")), "{said:#?}"); + } + + /// A reader 8 ms late to every interrupt, behind a TrackPoint held off + /// while the buffer is full: the bytes arrive 9 ms apart, past + /// `PACKET_GAP_NS`, and the device's own gaps are 1 ms. + #[test] + fn a_packet_survives_a_reader_later_than_the_packet_gap() { + let mut fake = Fake::t14(); + let mut stream = armed(&mut fake); + fake.latency = 8 * MS; + let at = fake.now + MS; + // Left held, 5 right and 3 up. + fake.aux_sends(at, &[0x09, 0x05, 0x03]); + let (records, _) = run(&mut fake, &mut stream, at + 100 * MS); + let motion = Motion::Relative { dx: 5, dy: -3 }; + assert_eq!(records, [Record::Pointer(Report { source: 0, buttons: 1, scroll: 0, motion })]); + } +} diff --git a/system/supervisor/src/main.rs b/system/supervisor/src/main.rs index d1b886dd649..408390b4947 100644 --- a/system/supervisor/src/main.rs +++ b/system/supervisor/src/main.rs @@ -93,7 +93,7 @@ use toyos_manifest::consent; use toyos_manifest::grants::{self, Answer, Decision, Store}; use toyos_manifest::launch::{self as authority, Authority, Session, Sessions, Target}; use toyos_manifest::package::{self, Package}; -use toyos_manifest::{Manifest, Program, View}; +use toyos_manifest::{Manifest, Program, Start, View}; use toyos::endow::Endowments; use toyos::fs::{Access, Grant, CAPABILITY_PREFIX}; use toyos::ipc::{self, Connection, RxStep}; @@ -639,8 +639,8 @@ struct Kept { /// A swap owns the service: its process ending is expected and the port /// stays open for the next one. swapping: bool, - /// The row says the supervisor starts it again when it ends. - restart: bool, + /// The row, which says what its end is owed ([`Program::supervised`]). + row: Program, /// It ended, and the loop owes it a new process on the same ports. ended: bool, /// The write end of [`Supervisor::wake`]. @@ -665,7 +665,7 @@ impl<'a> Service<'a> { acceptors, generation: 0, swapping: false, - restart: program.restart, + row: program.clone(), ended: false, wake, })), @@ -723,7 +723,7 @@ impl<'a> Service<'a> { )?; kept.generation += 1; kept.ended = false; - if !kept.acceptors.is_empty() { + if self.program.supervised(Start::Spawned) { let process = toyos_abi::syscall::dup(toyos::RawHandle(child.as_raw_handle())) .unwrap_or_else(|e| panic!("supervisor: {}'s process handle: {e:?}", self.program.name)); let (shared, generation) = (Arc::clone(&self.kept), kept.generation); @@ -779,11 +779,12 @@ impl std::fmt::Display for StartError { /// Wait for one start of a service to end, and close its ports if nothing was /// expecting it to, or wake the loop to start it again if its row says so. fn close_when_it_ends(kept: &Mutex, generation: u64, process: toyos::RawHandle) { - let _ = toyos_abi::syscall::process_wait(process); + let exit = toyos_abi::syscall::process_wait(process) + .unwrap_or_else(|e| panic!("supervisor: a service's own process handle refused its wait: {e:?}")); toyos_abi::syscall::close(process); let mut kept = kept.lock().expect("supervisor: a service's state is poisoned"); if kept.generation == generation && !kept.swapping { - if kept.restart { + if kept.row.supervised(Start::Ended(exit)) { kept.ended = true; // A full pipe is a wake already pending, which is all this is. let _ = toyos_abi::syscall::write_nonblock(kept.wake, &[1]); diff --git a/tests/checks.rs b/tests/checks.rs index 598bd35a766..674feb7d68f 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -507,7 +507,7 @@ mod checks { let xhci = if cpu == 0 { 40 } else { 0 }; format!( "[ {at} cpu{on} kernel] irq: cpu{cpu} timer=100 kick={kick} xhci={xhci} userdev=0 \ - i8042=0 dmafault=0 hda=0 tlb={tlb} nmi=0 spurious=0 unclaimed=0\n" + dmafault=0 hda=0 tlb={tlb} nmi=0 spurious=0 unclaimed=0\n" ) }; let issued = |at: &str, on: u32, total: u64| { @@ -1021,7 +1021,7 @@ mod checks { assert!(judge(&[&readback("jobcase", &done(stopped), jobcase)]).is_err()); // What the seal itself writes of the machine, above the ring's tail. - let census = "| irq: cpu0 timer=9 kick=2 xhci=1729 userdev=0 i8042=0 dmafault=0 hda=0 tlb=0 \ + let census = "| irq: cpu0 timer=9 kick=2 xhci=1729 userdev=0 dmafault=0 hda=0 tlb=0 \ nmi=0 spurious=0 unclaimed=0\n\ | tlb: shootdowns=4 wait=12us max=3us dlopen=0 pcid=0 mmio=0 unmap=4 pipe=0 staged=0 \ bench=0\n\ diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs index f975ac0fb95..ac11cd491e0 100644 --- a/tests/common/irqcensus.rs +++ b/tests/common/irqcensus.rs @@ -15,17 +15,15 @@ use std::collections::BTreeMap; /// and [`Census::parse`] refuses a line whose fields are not exactly these, so /// a source added on one side and not the other is a red rather than a silently /// dropped column. -pub const SOURCES: [&str; 11] = [ - "timer", "kick", "xhci", "userdev", "i8042", "dmafault", "hda", "tlb", "nmi", "spurious", - "unclaimed", -]; +pub const SOURCES: [&str; 10] = + ["timer", "kick", "xhci", "userdev", "dmafault", "hda", "tlb", "nmi", "spurious", "unclaimed"]; /// The sources whose delivery CPU is chosen by the interrupt controller rather /// than by the CPU that took the work — every device vector, in other words. /// `MSG_ADDR` names physical destination 0 and the one I/O APIC pin this kernel /// routes goes to the BSP, so today every one of these is cpu0's alone. The day /// that stops being true is the day the track's change lands. -pub const DEVICE_SOURCES: [&str; 5] = ["xhci", "userdev", "i8042", "dmafault", "hda"]; +pub const DEVICE_SOURCES: [&str; 4] = ["xhci", "userdev", "dmafault", "hda"]; /// One CPU's counters out of one `irq:` line. #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/tests/common/isa.rs b/tests/common/isa.rs index 423161ca4b9..44b13a8ecff 100644 --- a/tests/common/isa.rs +++ b/tests/common/isa.rs @@ -4,10 +4,6 @@ use super::serial::Serial; -/// The kernel's line under `i8042-withheld`: the premise of every row that -/// needs the claim granted. -pub const WITHHELD: &str = "i8042: withheld, left unprobed for a claim"; - /// How often `needle` is in `log`, which must be `want`. fn said(log: &Serial, needle: &str, want: usize) -> Result<(), String> { match log.text().matches(needle).count() { @@ -16,14 +12,13 @@ fn said(log: &Serial, needle: &str, want: usize) -> Result<(), String> { } } -/// `test_rs_isa_grant` on a boot whose i8042 the kernel left alone: every -/// access its children make that the kernel must refuse, named by the kernel -/// with the port and whose grant it is not. The unbound holder and the moved +/// `test_rs_isa_grant` on a boot that starts no ps2server: every access its +/// children make that the kernel must refuse, named by the kernel with the +/// port and whose grant it is not. The unbound holder and the moved /// claim both die at 0x64, the bound children one port past what they hold by /// an `in` and by an `out`, the wide one on the port its access spans past the /// grant, and the process holding nothing at the data port. pub fn ports(kernel: &Serial) -> Result<(), String> { - kernel.must_say(WITHHELD)?; const NAMED: [(&str, usize); 5] = [ ("in of 1 byte(s) from port 0x0064", 2), ("in of 1 byte(s) from port 0x0061", 1), @@ -40,8 +35,8 @@ pub fn ports(kernel: &Serial) -> Result<(), String> { } /// `test_rs_isa_lines` on the same boot: one holder read a first interrupt off -/// its claim, and no later claim did. +/// its claim, and no later claim did, the one after a holder that died with a +/// record unread among them. pub fn lines(kernel: &Serial) -> Result<(), String> { - kernel.must_say(WITHHELD)?; said(kernel, "isa: the i8042 took its first interrupt", 1) } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 24a72a3500e..0fe40a9630f 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -14,6 +14,8 @@ pub mod irqcensus; pub mod isa; pub mod metal; pub mod power; +/// What ps2server says of the i8042 it armed. +pub mod ps2server; pub mod qemu; pub mod screen; pub mod serial; diff --git a/tests/common/ps2server.rs b/tests/common/ps2server.rs new file mode 100644 index 00000000000..9d5439a628c --- /dev/null +++ b/tests/common/ps2server.rs @@ -0,0 +1,42 @@ +//! What `/system/bin/ps2server` says of the i8042 it armed, read the same way +//! off a QEMU boot's console and a metal boot's log. + +use super::irqcensus::Census; +use super::serial::Serial; + +/// The probe's line where the keyboard and a pointer were armed. +pub const ARMED_BOTH: &str = "ps2server: armed, command byte 0x57 — keyboard on, aux on"; +/// Where the keyboard was armed and the aux device found absent. +pub const ARMED_KEYBOARD: &str = "ps2server: armed, command byte 0x75 — keyboard on, aux off"; +/// The keyboard's reset, which names its `0xAA`'s time after the `0xFA`. +pub const KEYBOARD_RESET: &str = "ps2server: kbd reset, 0xfa "; +const AUX_ARMED: &str = "ps2server: aux reset, 0xaa "; +const AUX_ABSENT: &str = "ps2server: aux absent — its reset went unanswered in 600 ms"; + +/// `log` says ps2server reset the keyboard and armed it set 2 translated, and +/// armed the aux device or found it absent, each by its line; nothing it said +/// is a refusal; and the kernel's interrupt census has no source of its own +/// for the i8042, which [`Census::parse`] refuses a line carrying. +pub fn armed(log: &Serial, kernel: &Serial) -> Result<(), String> { + let reset = log.must_say(KEYBOARD_RESET)?; + if !reset.ends_with("— set 2, translated") { + return Err(format!("the keyboard's reset line is not the armed one: {reset}")); + } + eprintln!(" [ps2server] {}", reset.trim()); + let aux = log + .must_say(AUX_ARMED) + .or_else(|_| log.must_say(AUX_ABSENT)) + .map_err(|_| format!("ps2server said neither {AUX_ARMED:?} nor {AUX_ABSENT:?}:\n{}", log.text()))?; + eprintln!(" [ps2server] {}", aux.trim()); + let armed = if aux.contains(AUX_ARMED) { ARMED_BOTH } else { ARMED_KEYBOARD }; + log.must_say(armed)?; + for refusal in ["ps2server: kbd DISABLED", "ps2server: aux DISABLED", "ps2server: DISABLED"] { + log.must_not_say(refusal)?; + } + for line in kernel.text().lines() { + if let Some(Err(why)) = Census::parse(line) { + return Err(format!("the census line {line:?} is not this kernel's sources: {why}")); + } + } + Ok(()) +} diff --git a/tests/guest/rust-tests/src/bin/acpi_mediated.rs b/tests/guest/rust-tests/src/bin/acpi_mediated.rs index 70ccf59067f..bbd9c2e4993 100644 --- a/tests/guest/rust-tests/src/bin/acpi_mediated.rs +++ b/tests/guest/rust-tests/src/bin/acpi_mediated.rs @@ -2,9 +2,8 @@ //! kernel reads and writes for it, what it refuses and by which name, and the //! firmware's Global Lock taken, found owned and given back. //! -//! Run on a boot that starts no ACPI server (`tests/images/acpicase.toml`) and whose -//! kernel leaves the i8042 to a claim (`i8042-withheld`), so its row is -//! another claim's; on a guest: every write here that must be refused is one +//! Run on a boot that starts no ACPI server (`tests/images/acpicase.toml`), +//! where the i8042's row is another claim's; on a guest: every write here that must be refused is one //! a kernel that made it would make for real — to RAM, to the tables, to //! COM1, to `PM1a_CNT`, to a function's configuration space — and the //! firmware's side of the lock is staged on the FACS itself, which only a @@ -322,8 +321,8 @@ fn ports(holder: &Holder, info: &AcpiInfo) { assert_eq!(holder.read(Space::SystemIo, 0x1_0000, Width::Byte), Err(Refused::PortSpan)); assert_eq!(holder.read(Space::SystemIo, 0x80, Width::QWord), Err(Refused::PortSpan)); - // The i8042's row, which another claim is for: this boot's kernel leaves - // the controller unprobed, so nothing declared its ports. + // The i8042's row, which another claim is for: no kernel drives the + // controller, so nothing declared its ports. for port in [0x60u64, 0x64] { assert_eq!(holder.read(Space::SystemIo, port, Width::Byte), Err(Refused::ClaimedPort), "acpi: port {port:#x} was read"); assert_eq!(holder.write(Space::SystemIo, port, Width::Byte, 0), Err(Refused::ClaimedPort), "acpi: port {port:#x} was written"); diff --git a/tests/guest/rust-tests/src/bin/isa_claim_refused.rs b/tests/guest/rust-tests/src/bin/isa_claim_refused.rs deleted file mode 100644 index 77546a24f04..00000000000 --- a/tests/guest/rust-tests/src/bin/isa_claim_refused.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Where the kernel drives the i8042, an `isa` claim on it is refused: two -//! drivers on one controller is not something a claim may create. - -use toyos::endow::Endowments; -use toyos::syscap::SysCap; -use toyos::Device; -use toyos_abi::syscall::{IsaId, SyscallError, SYSCAP_LABEL}; - -fn main() { - let cap: SysCap = Endowments::get() - .take(SYSCAP_LABEL) - .expect("the test estate is endowed a device-minting capability"); - let set = IsaId::parse("0060,0064:1,12").expect("the i8042's set"); - match cap.claim_isa::(set) { - Err(SyscallError::PermissionDenied) => {} - other => panic!( - "a claim on the i8042 answered {:?}, not PermissionDenied: the kernel does not drive \ - the controller here, or let a claim share it", - other.map(|_| ()) - ), - } - println!("the claim on a controller the kernel drives was refused PermissionDenied"); -} diff --git a/tests/guest/rust-tests/src/bin/isa_grant.rs b/tests/guest/rust-tests/src/bin/isa_grant.rs index 0045f58d730..83019775ccc 100644 --- a/tests/guest/rust-tests/src/bin/isa_grant.rs +++ b/tests/guest/rust-tests/src/bin/isa_grant.rs @@ -1,8 +1,7 @@ //! The `isa` claim's ports: the i8042's two, opened in the I/O permission //! bitmap for the one process that bound them and for no other. //! -//! Run where the kernel drives no i8042 (`i8042-withheld`), so the claim is -//! granted. Every port access that must be refused is made in a child of its +//! Run on a boot that starts no ps2server, so the claim is granted. Every port access that must be refused is made in a child of its //! own, which prints a marker, makes the one access and must never print again; //! the kernel's record of each kill is the harness's to read. //! diff --git a/tests/guest/rust-tests/src/bin/isa_lines.rs b/tests/guest/rust-tests/src/bin/isa_lines.rs index 7eac42c5106..24fc315be4f 100644 --- a/tests/guest/rust-tests/src/bin/isa_lines.rs +++ b/tests/guest/rust-tests/src/bin/isa_lines.rs @@ -1,14 +1,18 @@ //! The `isa` claim's lines: an interrupt the i8042 raises reaches the claim's -//! holder as a record, and one it raises once the claim is gone reaches nobody. +//! holder as a record, one it raises once the claim is gone reaches nobody, +//! and a holder that dies takes its record with it. //! -//! Run where the kernel drives no i8042 (`i8042-withheld`), with no key to -//! press: the keyboard is told to enable scanning (`0xF4`) and its -//! acknowledgement (`0xFA`) is the byte that raises the line. +//! Run on a boot that starts no ps2server, with no key to press: the keyboard +//! is told to enable scanning (`0xF4`) and its acknowledgement (`0xFA`) is the +//! byte that raises the line. //! //! - `device` holds the claim: it drives the controller until the //! acknowledgement arrives as a record and a byte, gives the claim up, and //! has the keyboard acknowledge once more, on a line then masked. -//! - `after` holds the next claim on the row, and finds no record on it. +//! - `dies` holds the next claim: it has the keyboard acknowledge, so a record +//! is pending on the claim, and ends without reading it. +//! - `after` holds the claim after that, and finds no record on it: the next +//! claimant is handed the row and none of the dead holder's interrupts. use std::os::toyos::process::CommandExt; use std::process::Command; @@ -60,6 +64,7 @@ fn main() { match std::env::args().nth(1).as_deref() { None => lines(&syscap()), Some("device") => device(taken()), + Some("dies") => dies(taken()), Some("after") => after(taken()), other => panic!("isa_lines: unknown role {other:?}"), } @@ -93,8 +98,13 @@ fn lines(cap: &SysCap) { role("device", isa_row::claim_after(cap, set(), "the job before this one")); // The row comes back with `device`, whose keyboard acknowledged once more // after its claim was gone. - role("after", isa_row::claim_after(cap, set(), "device")); - println!("isa lines: the holder's interrupt reached it, and the one after its claim reached nobody"); + role("dies", isa_row::claim_after(cap, set(), "device")); + // And with `dies`, which ended holding a record it never read. + role("after", isa_row::claim_after(cap, set(), "dies")); + println!( + "isa lines: the holder's interrupt reached it, the one after its claim reached nobody, and a \ + dead holder's record went with it" + ); } fn inb(port: u16) -> u8 { @@ -182,18 +192,7 @@ fn arm(poller: &Poller, claim: &Device) { /// acknowledge again. fn device(claim: Device) { bind(&claim); - drain(); - // Read the configuration byte, then write it back with the keyboard's line - // on and its clock running, and the aux port's clock stopped so that every - // byte from here on is the keyboard's (i8042: 0x20 reads it, 0x60 writes - // it, 0xAE enables the keyboard's port). - command(0x20); - wait_status("answered its configuration", |s| s & OBF != 0); - let config = inb(DATA); - command(0x60); - data((config | CFG_KEYBOARD_IRQ | CFG_AUX_CLOCK_OFF) & !CFG_KEYBOARD_CLOCK_OFF); - command(0xAE); - drain(); + let config = line_on(); println!("isa device: config {config:#04x}, keyboard line on"); // Armed before the keyboard is asked anything, so the watch is completed @@ -222,14 +221,47 @@ fn device(claim: Device) { println!("isa device: the keyboard acknowledged once more after the claim went"); } -/// The claim after `device`'s: nothing raised the line under it, so it holds -/// no record. +/// Read the configuration byte, then write it back with the keyboard's line +/// on and its clock running, and the aux port's clock stopped so that every +/// byte from here on is the keyboard's (i8042: 0x20 reads it, 0x60 writes it, +/// 0xAE enables the keyboard's port); answer the byte it came with. +fn line_on() -> u8 { + drain(); + command(0x20); + wait_status("answered its configuration", |s| s & OBF != 0); + let config = inb(DATA); + command(0x60); + data((config | CFG_KEYBOARD_IRQ | CFG_AUX_CLOCK_OFF) & !CFG_KEYBOARD_CLOCK_OFF); + command(0xAE); + drain(); + config +} + +/// Hold the claim with the keyboard's line on, have the keyboard acknowledge, +/// and end once the claim holds a record of it, unread. +fn dies(claim: Device) { + bind(&claim); + let config = line_on(); + let poller = Poller::new(1); + arm(&poller, &claim); + acknowledged("with a record pending"); + let mut woke = false; + poller.wait(1, RECORD.as_nanos() as u64, |_| woke = true); + assert!(woke, "isa dies: no record in {RECORD:?} of the keyboard's acknowledgement"); + // The configuration the controller came with; the record stays unread. + command(0x60); + data(config); + println!("isa dies: ending with the keyboard's acknowledgement unread on the claim"); +} + +/// The claim after `dies`'s: the record it left went with its claim, and +/// nothing raised the line since, so it holds none. fn after(claim: Device) { bind(&claim); if let Some(count) = record(&claim) { panic!( "isa after: a claim nothing raised a line under carries a record of {count}: the line \ - was open while no claim held it" + was open while no claim held it, or a dead holder's record outlived its claim" ); } println!("isa after: no record on a claim nothing raised a line under"); diff --git a/tests/images/metalcase.toml b/tests/images/metalcase.toml index bc04f3dc996..24118a6992d 100644 --- a/tests/images/metalcase.toml +++ b/tests/images/metalcase.toml @@ -9,7 +9,7 @@ assets = ["assets"] [boot] -start = ["logkeeper", "diskserver", "fileserver", "compositor", "soundserver", "netstack", "sshserver", "test-runner"] +start = ["logkeeper", "diskserver", "fileserver", "compositor", "soundserver", "netstack", "sshserver", "test-runner", "ps2server"] # **Every image that carries a `TOYOS-LOG` partition runs this**, and every # image does. The kernel keeps the record ring and writes no file at all, so a @@ -24,7 +24,7 @@ syscap = ["logread"] [programs.compositor] service = true -serves = ["compositor", "prompt"] +serves = ["compositor", "prompt", "input"] receives = ["soundserver"] devices = ["framebuffer", "keyboard", "mouse"] @@ -45,6 +45,17 @@ devices = ["pci:1af4:1041"] service = true receives = ["netstack"] +# The i8042, driven from userland: its ports and its two lines are this +# program's `isa` claim, and the keys and pointer reports it decodes go to the +# owner of input over `input`, which no other row may receive +# (`build/build.rs`). Started again when it ends other than by exit 0, which it +# says only on a machine with nothing to serve. +[programs.ps2server] +restart = true +receives = ["input"] +devices = ["isa:0060,0064:1,12"] + + # The union its guest binaries need on this machine shape. [programs.test-runner] receives = ["compositor", "soundserver", "netstack"] diff --git a/tests/images/panelcase.toml b/tests/images/panelcase.toml index 39a6dd60c7b..e7c28434bd7 100644 --- a/tests/images/panelcase.toml +++ b/tests/images/panelcase.toml @@ -16,5 +16,5 @@ syscap = ["logread"] [programs.compositor] service = true -serves = ["compositor", "prompt"] +serves = ["compositor", "prompt", "input"] devices = ["framebuffer", "keyboard", "mouse"] diff --git a/tests/metal/src/lib.rs b/tests/metal/src/lib.rs index 393d96fccd2..629685e2b7f 100644 --- a/tests/metal/src/lib.rs +++ b/tests/metal/src/lib.rs @@ -752,11 +752,9 @@ pub const FLASHABLE: &[&str] = &[ "unclaimed-vector-selftest", "xhci-xecp-selftest", "xhci-descriptor-selftest", - // Two probes rather than staged inputs, and both are reads: the SS-reload - // one runs inside the first syscall's own context switch, and the input-core one merges - // events it made up itself. + // A probe rather than staged inputs, and a read: it runs inside the first + // syscall's own context switch. "sysret-ss-probe", - "test-input-merge", // Three nested `scheduler::Operation`s with known deadlines, in both homes, // each printing what it asked for and what it observed. It establishes and // drops them and reaches nothing else. @@ -801,12 +799,6 @@ pub const FLASHABLE: &[&str] = &[ // hold, reaches no firmware state, and the worst // it leaves is a stick a replug clears — the defect the arm exists to stage. "usb-transport-break", - // The kernel leaves the i8042 unprobed and a test process drives it through - // an `isa` claim: it reads the configuration byte, has the keyboard - // acknowledge `0xF4` and writes the byte back, each a command this kernel's - // own driver sends the controller on every boot. It reaches no firmware - // state, and firmware programs the controller again at the next power-on. - "i8042-withheld", // The kernel leaves the Type-C xHCI controller to a claim, and `usbd` // drives it through the unit: the handoff, a reset and its own rings, as // this kernel's driver does every boot. Firmware owns neither of the diff --git a/tests/toyos.rs b/tests/toyos.rs index 564a66606b8..f07530d619f 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -14,7 +14,7 @@ use common::qemu::{ self, await_guest, await_marker, BootOptions, QemuInstance, STALLED, TIMED_OUT, }; -use common::{audio, claims, compile, devices, faults, isa, metal, power, screen, serial, usb}; +use common::{audio, claims, compile, devices, faults, isa, metal, power, ps2server, screen, serial, usb}; use toyos_build::bootlog::{self}; use toyos_metal::testargs::{self, SUITE}; @@ -108,7 +108,7 @@ const RUST_SKIP: &[&str] = &[ "ccheck", "disk_backtrace_child", "fault_gate_child", - // Each needs a boot whose i8042 the kernel does not drive: the + // Each drives the machine's own i8042 through its `isa` claim: the // `isa_ports_are_the_binders_alone` and `isa_lines_reach_their_holder` // metal rows run them. "isa_grant", @@ -161,9 +161,8 @@ const RUST_SKIP: &[&str] = &[ // `acpi_server_events` metal row runs it. "acpi_counted", // It claims the fixed hardware itself, which needs a boot that starts no - // server, and stages the firmware's side of the Global Lock and finds the - // i8042's row another claim's, which need the test kernel and its - // `i8042-withheld`: `acpi_mediated_access` and `acpi_press_is_one_sci` + // server, and stages the firmware's side of the Global Lock, which needs + // the test kernel: `acpi_mediated_access` and `acpi_press_is_one_sci` // run it on tests/images/acpicase.toml. "acpi_mediated", // It powers the machine off: `machine_shutdown_short_stop` runs it. @@ -809,6 +808,14 @@ const METAL: &[(&str, metal::Metal)] = &[ "metal_sim_scanout_wc", metal::Metal { arms: METALCASE, judge: |b| scanout_wc(b[0].kernel().text()) }, ), + ( + // The machine's own i8042 and EC, driven from userland: the keyboard + // reset and armed set 2 translated, its `0xAA`'s time named, the aux + // device armed or found absent by name, and no i8042 source in the + // kernel's interrupt census. + "ps2server_arms", + metal::Metal { arms: METALCASE, judge: |b| ps2server::armed(&b[0].log(), &b[0].kernel()) }, + ), ( // Two boots the suite already flashes: the device boot writes and // fsyncs megabytes before its reset, and `metalcase` is the same reset @@ -882,10 +889,6 @@ const METAL: &[(&str, metal::Metal)] = &[ "sysret_ss_reload", metal::Metal { arms: SELFTESTS, judge: |b| sysret_ss(b[0].kernel().text()) }, ), - ( - "input_merge", - metal::Metal { arms: SELFTESTS, judge: |b| input_merge_ok(b[0].kernel().text()) }, - ), ( "operation_nesting", metal::Metal { @@ -948,14 +951,14 @@ const METAL: &[(&str, metal::Metal)] = &[ }, }, ), - // ---- the `isa` claim: one image whose i8042 the kernel leaves alone ---- + // ---- the `isa` claim: the machine's i8042, driven by a test process ---- ( // The I/O permission bitmap on the machine's own processor: the ports // open to the process that bound them, and every other access killed // by name. "isa_ports_are_the_binders_alone", metal::Metal { - arms: ISA_WITHHELD, + arms: ISA, judge: |b| { b[0].job_passed("test_rs_isa_grant")?; isa::ports(&b[0].kernel()) @@ -967,7 +970,7 @@ const METAL: &[(&str, metal::Metal)] = &[ // the claim's holder, and to nobody once the claim is gone. "isa_lines_reach_their_holder", metal::Metal { - arms: ISA_WITHHELD, + arms: ISA, judge: |b| { b[0].job_passed("test_rs_isa_lines")?; isa::lines(&b[0].kernel()) @@ -986,12 +989,13 @@ const METAL: &[(&str, metal::Metal)] = &[ ), ]; -/// A boot whose kernel leaves the i8042 unprobed, so the one grantable row is -/// free: the ports' job first, since its last holder keeps them until it ends. -const ISA_WITHHELD: &[metal::Arm] = &[metal::once( - "isa-withheld", +/// The plain `tests/images/testcases.toml` boot, which starts no ps2server, so +/// the one grantable row is free: the ports' job first, since its last holder +/// keeps them until it ends. +const ISA: &[metal::Arm] = &[metal::once( + "isa", "tests/images/testcases.toml", - &["i8042-withheld"], + &[], &["test_rs_isa_grant", "test_rs_isa_lines"], )]; @@ -1093,7 +1097,6 @@ const SELFTEST_PARAMS: &[&str] = &[ "xhci-xecp-selftest", "xhci-descriptor-selftest", "sysret-ss-probe", - "test-input-merge", "sched-operation-nesting", "iommu-firmware-left", ]; @@ -1820,8 +1823,8 @@ fn acpi_mediated_access(cpus: u32) -> Result<(), String> { &[], &[], BootOptions { - // The test kernel, for the Global Lock's actuator too. - kernel_params: &["i8042-withheld"], + // The test kernel, for the Global Lock's actuator. + kernel_features: ACTUATOR_KERNEL, ready_marker: "acpi: the ACPI row: ", smp: cpus, extra_root_files: vec![suite_bin(toyos_build::arch::Arch::X86_64, "acpi_mediated")], @@ -1839,7 +1842,6 @@ fn acpi_mediated_access(cpus: u32) -> Result<(), String> { })?; let said = serial::Serial::named("the probe's boot", console); said.must_be_clean()?; - said.must_say(isa::WITHHELD)?; said.must_say("acpi: the Global Lock is the FACS's at ")?; said.must_say("acpi: the Global Lock given back for a holder that left it taken (its claim is gone)")?; // Whose range registers passed the unlisted read, and how this @@ -1988,7 +1990,8 @@ fn acpi_press_is_one_sci() -> Result<(), String> { /// Boot the image `--console-boot` builds, on the machine shape with a panel /// for `/system/bin/console` to claim, and wait for what that image owes at -/// boot: the console up on its panel, having read the log and the keyboard. +/// boot: the console up on its panel, having read the log and the keyboard, +/// and ps2server's i8042 armed, its keyboard and QEMU's PS/2 mouse both. /// Nothing is typed at its shell, whose only input is the i8042: /// `issues/the-console-loses-typed-keystrokes-under-host-load.md`. fn console_image_boots() -> Result<(), String> { @@ -2000,7 +2003,10 @@ fn console_image_boots() -> Result<(), String> { ); let mut console = format!("{}\n", qemu.boot_log()); await_marker(&mut qemu, &mut console, "console: ready ", "the console to take its panel")?; - serial::Serial::named("the console image's boot", console).must_be_clean() + await_marker(&mut qemu, &mut console, ps2server::ARMED_BOTH, "ps2server to arm the i8042")?; + let said = serial::Serial::named("the console image's boot", console); + eprintln!(" [ps2server] {}", said.must_say(ps2server::KEYBOARD_RESET)?.trim()); + said.must_be_clean() } /// Boot `tests/images/panelcase.toml` on [`qemu::Profile::MetalNoX2apic`]'s CPUs, which @@ -4036,14 +4042,6 @@ fn sysret_ss(log: &str) -> Result<(), String> { Ok(()) } -/// The input core merged what it was handed. -fn input_merge_ok(log: &str) -> Result<(), String> { - if !log.contains("input-merge: ok") { - return Err(format!("the input core check never reported:\n{log}")); - } - Ok(()) -} - /// Every unit this kernel went on to program, and every AMD-Vi unit it /// switched off, said it was handed over with what the actuator left on. /// From 6016e657b002bbbfdddac29590f7af7510cf5a73 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 08:21:37 +0200 Subject: [PATCH 2/8] ps2server: the aux stage's command byte alone disables the keyboard, and metalcase holds until the probe ends The first metal run found two reds, neither of them the probe's: - `ps2server_arms`, on both benches: `metalcase` runs no job, so its test-runner rebooted 3 ms after the supervisor started ps2server (T14, inside the keyboard's 516 ms reset) and 1 ms after the keyboard's line (AMD, inside the aux stage's 600 ms). The kernel's probe ran during boot; ps2server's runs after it. `test_rs_ps2server_hold`, on `acpi_hold`'s pattern, ends every `metalcase` boot once ps2server has said it armed or the kernel has recorded its exit; the image serves test-runner `log`. - `isa_lines_reach_their_holder`: a claim's first interrupt is announced once per claim (the release re-arms it), and `dies` read away the stale record its own `0x20` raised under firmware's `0x47`, so the kernel announced it twice. `dies` now reads no record at all, which is also what its name says, and the count of one holds on any firmware byte. The orchestrator's ruling on m06 removes the `0xAD` after the keyboard stage: the aux stage's `0x60` sets bit 4, which disables the keyboard interface as `0xAD` does (C 11, C 12). The dropped-command-byte shape now drops the arming's byte alone, so the aux stage's bit 4 is what the `0x20` runs under, and is what it reads back (`0x56`). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/ps2server/src/fake.rs | 8 ++-- system/ps2server/src/probe.rs | 28 +++++++------- tests/guest/rust-tests/src/bin/isa_lines.rs | 7 ++-- .../rust-tests/src/bin/ps2server_hold.rs | 37 +++++++++++++++++++ tests/images/metalcase.toml | 13 ++++--- tests/toyos.rs | 17 +++++++-- 6 files changed, 79 insertions(+), 31 deletions(-) create mode 100644 tests/guest/rust-tests/src/bin/ps2server_hold.rs diff --git a/system/ps2server/src/fake.rs b/system/ps2server/src/fake.rs index 12ea3797d6f..a2c39e765f4 100644 --- a/system/ps2server/src/fake.rs +++ b/system/ps2server/src/fake.rs @@ -105,8 +105,8 @@ pub struct Fake { pub aux: Aux, /// Nothing decodes the ports. pub absent: bool, - /// The controller drops a `0x60`'s byte. - pub drops_command_byte: bool, + /// The controller drops a `0x60` whose byte is this one. + pub drops_command_byte: Option, /// The output buffer is full again the moment it is read. pub wedged: bool, /// The keyboard's bytes are translated whatever bit 6 says. @@ -147,7 +147,7 @@ impl Fake { sends: VecDeque::new(), }, absent: false, - drops_command_byte: false, + drops_command_byte: None, wedged: false, translates_always: false, latency: 0, @@ -414,7 +414,7 @@ impl Controller for Fake { fn write(&mut self, byte: u8) { self.written(&format!("the data byte {byte:#04x}")); match self.arg.take() { - Some(Arg::CommandByte) if self.drops_command_byte => {} + Some(Arg::CommandByte) if self.drops_command_byte == Some(byte) => {} Some(Arg::CommandByte) => { if self.cb & 0x10 != 0 && byte & 0x10 == 0 { self.kbd_enabled_at = self.now; diff --git a/system/ps2server/src/probe.rs b/system/ps2server/src/probe.rs index ce79221000a..ed2d940e359 100644 --- a/system/ps2server/src/probe.rs +++ b/system/ps2server/src/probe.rs @@ -14,12 +14,12 @@ //! (C 10), set 2 is set 1 on the wire, whatever set firmware or a //! predecessor left. The `0xAA` is taken whenever it comes inside the //! stage's ceiling: K 11's 300-500 ms window decides nothing here. -//! 3. **`0xAD`:** the keyboard holds what it would send (K88 39). -//! 4. **Aux:** the command byte with the aux interrupt alone and the keyboard -//! interface still disabled, `0xA8`; `0xD4 0xFF`, answered `0xFA 0xAA` -//! and its id (TP 9, 12), and `0xD4 0xF4` (TP 13). A device that does not -//! answer its reset is absent. -//! 5. **Arming:** the command byte whole, read back with `0x20` while the +//! 3. **Aux:** the command byte with the aux interrupt alone and the keyboard +//! interface disabled again, so the keyboard holds what it would send +//! (K88 39); `0xA8`; `0xD4 0xFF`, answered `0xFA 0xAA` and its id (TP 9, +//! 12), and `0xD4 0xF4` (TP 13). A device that does not answer its reset +//! is absent. +//! 4. **Arming:** the command byte whole, read back with `0x20` while the //! keyboard interface is disabled, so the answer is the controller's; then //! `0xAE`, and the keyboard sends what it held. //! @@ -31,8 +31,7 @@ use crate::bus::*; -/// The controller-only steps' ceiling: the quiet stage, each `0xAD`, the -/// arming. +/// The controller-only stages' ceiling: the quiet stage and the arming. const CONTROLLER_NS: u64 = 250_000_000; /// The keyboard stage's, inside which its reset's `0xAA` is taken. pub const KEYBOARD_NS: u64 = 750_000_000; @@ -98,8 +97,6 @@ impl Probe<'_, C> { fn run(&mut self) -> Result { self.quiet()?; let keyboard = self.keyboard()?; - let deadline = self.deadline(CONTROLLER_NS); - self.command(DISABLE_KEYBOARD, deadline, "0xAD after the keyboard stage")?; let aux = self.aux()?; if !keyboard && !aux { (self.say)("ps2server: neither the keyboard nor a pointer to serve".to_string()); @@ -228,7 +225,7 @@ impl Probe<'_, C> { Ok(true) } - /// Step 4: whether the aux device reports. + /// Step 3: whether the aux device reports. fn aux(&mut self) -> Result { let deadline = self.deadline(AUX_NS); self.command_byte(AUX_STAGE, deadline)?; @@ -280,7 +277,7 @@ impl Probe<'_, C> { Ok(false) } - /// Step 5. + /// Step 4. fn arm(&mut self, keyboard: bool, aux: bool) -> Result<(), Stopped> { let deadline = self.deadline(CONTROLLER_NS); let mut byte = CB_SYSTEM_FLAG | CB_KEYBOARD_DISABLED | CB_TRANSLATE; @@ -494,14 +491,15 @@ mod tests { assert!(fake.commands.is_empty(), "a byte written to ports nothing decodes: {:02x?}", fake.commands); } + /// The arming's own byte dropped: the aux stage's is what keeps the + /// keyboard interface disabled at the `0x20`, and is read back. #[test] fn a_command_byte_the_controller_dropped_is_said_by_name() { let mut fake = Fake::t14(); - fake.cb = 0x07; - fake.drops_command_byte = true; + fake.drops_command_byte = Some(0x57); let (probed, said) = probed(&mut fake); assert_eq!(probed, Probed::Failed); - line(&said, "ps2server: DISABLED — command byte 0x57 did not take (read back 0x17)"); + line(&said, "ps2server: DISABLED — command byte 0x57 did not take (read back 0x56)"); } #[test] diff --git a/tests/guest/rust-tests/src/bin/isa_lines.rs b/tests/guest/rust-tests/src/bin/isa_lines.rs index 24fc315be4f..84cb19310f7 100644 --- a/tests/guest/rust-tests/src/bin/isa_lines.rs +++ b/tests/guest/rust-tests/src/bin/isa_lines.rs @@ -10,7 +10,7 @@ //! acknowledgement arrives as a record and a byte, gives the claim up, and //! has the keyboard acknowledge once more, on a line then masked. //! - `dies` holds the next claim: it has the keyboard acknowledge, so a record -//! is pending on the claim, and ends without reading it. +//! is pending on the claim, and ends having read no record. //! - `after` holds the claim after that, and finds no record on it: the next //! claimant is handed the row and none of the dead holder's interrupts. @@ -238,12 +238,13 @@ fn line_on() -> u8 { } /// Hold the claim with the keyboard's line on, have the keyboard acknowledge, -/// and end once the claim holds a record of it, unread. +/// and end once the claim holds a record, having read none: a read would +/// have the kernel announce this claim's first interrupt as `device`'s did. fn dies(claim: Device) { bind(&claim); let config = line_on(); let poller = Poller::new(1); - arm(&poller, &claim); + poller.watch(&claim, READABLE, 0); acknowledged("with a record pending"); let mut woke = false; poller.wait(1, RECORD.as_nanos() as u64, |_| woke = true); diff --git a/tests/guest/rust-tests/src/bin/ps2server_hold.rs b/tests/guest/rust-tests/src/bin/ps2server_hold.rs new file mode 100644 index 00000000000..c0c3f5dcfff --- /dev/null +++ b/tests/guest/rust-tests/src/bin/ps2server_hold.rs @@ -0,0 +1,37 @@ +//! Hold the boot open until `/system/bin/ps2server`'s probe has ended: it has +//! said it armed the i8042, or the kernel has recorded its exit. Exits +//! non-zero where the runner's own bound comes near first. +//! +//! The rows that read what ps2server said end their boot on this: its probe +//! waits on the keyboard's reset and the aux device's, the better part of a +//! second on the T14, and a boot that ends sooner cuts its lines off. + +use std::time::Duration; + +use toyos_logstream::{parse, program_line, Source}; + +#[path = "../served_log.rs"] +mod served_log; + +/// How long after boot the wait gives up: the runner's bound less a tenth. +const UNTIL_MS: u64 = toyos_tco::JOB_BOUND_MS - toyos_tco::JOB_BOUND_MS / 10; + +/// ps2server's line once it armed the controller, whatever it armed. +const ARMED: &str = "ps2server: armed, "; +/// The head of the kernel's record of ps2server's exit. +const EXITED: &str = "exit: ps2server pid="; + +fn main() { + let since_boot = toyos_abi::clock::nanos_since_boot() / 1_000_000; + let bound = Duration::from_millis(UNTIL_MS.saturating_sub(since_boot)); + let mut ended = String::new(); + served_log::Log::open().until("the end of ps2server's probe", bound, |line| { + let armed = program_line(line).is_some_and(|said| said.tag == "ps2server" && said.text.starts_with(ARMED)); + let exited = parse(line).is_some_and(|p| p.source == Source::Kernel && p.text.starts_with(EXITED)); + if armed || exited { + ended = line.trim_end().to_string(); + } + armed || exited + }); + println!("ps2server_hold: ps2server's probe ended, and nothing stopped the machine: {ended}"); +} diff --git a/tests/images/metalcase.toml b/tests/images/metalcase.toml index e0f04fbeb85..cddef6dfb16 100644 --- a/tests/images/metalcase.toml +++ b/tests/images/metalcase.toml @@ -15,12 +15,14 @@ start = ["logkeeper", "diskserver", "fileserver", "compositor", "soundserver", " # image does. The kernel keeps the record ring and writes no file at all, so a # boot config without `logkeeper` is a boot whose `/log` is empty — # `every_boot_config_runs_logkeeper` is what refuses one. -# It claims no device and serves no port: its row's authority is `logread`, -# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and the supervisor hands -# it every program's output beside that. +# It claims no device: its row's authority is `logread`, which is +# `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and the supervisor hands +# it every program's output beside that. `log` is the port it hands a reader +# the log on. [programs.logkeeper] service = true syscap = ["logread"] +serves = ["log"] [programs.compositor] service = true @@ -56,9 +58,10 @@ receives = ["input"] devices = ["isa:0060,0064:1,12"] -# The union its guest binaries need on this machine shape. +# The union its guest binaries need on this machine shape; `log` is what +# `ps2server_hold` reads ps2server's probe off. [programs.test-runner] -receives = ["compositor", "soundserver", "netstack"] +receives = ["compositor", "soundserver", "netstack", "log"] syscap = ["logread"] # The block service: the NVMe controller this machine's DATA is on, driven diff --git a/tests/toyos.rs b/tests/toyos.rs index 87c00c6dca5..619e43da662 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -156,6 +156,9 @@ const RUST_SKIP: &[&str] = &[ // has a battery: the `acpi_tables_loaded`, `acpi_server_events` and // `battery_read` metal rows end their boot on it. "acpi_hold", + // It waits for ps2server's probe to end, which only a boot that starts + // ps2server has: every `metalcase` row ends its boot on it. + "ps2server_hold", // It waits for the ACPI server's count of its embedded controller's // queries, and no guest's machine has that controller: the // `acpi_server_events` metal row runs it. @@ -805,7 +808,7 @@ const METAL: &[(&str, metal::Metal)] = &[ "app_view", metal::Metal { arms: PROCTREECASE, judge: |b| app_view(b[0]) }, ), - // ---- one image: tests/images/metalcase.toml, which runs no job ---- + // ---- one image: tests/images/metalcase.toml, whose one job is the hold ---- // // The rows after the first read what any boot that hands the machine back // writes, so they ride this one. @@ -824,7 +827,7 @@ const METAL: &[(&str, metal::Metal)] = &[ ( // Two boots the suite already flashes: the device boot writes and // fsyncs megabytes before its reset, and `metalcase` is the same reset - // with no job behind it. + // with nothing written behind it but the hold's line. "usb_reset_hands_devices_back", metal::Metal { arms: USB_RESET_BOOTS, @@ -1062,7 +1065,8 @@ fn windows_load_exited() -> String { } /// The two boots the reset ruling is judged on: the device boot for a reset -/// with megabytes behind it, and `metalcase` for one with no job behind it. +/// with megabytes behind it, and `metalcase` for one with nothing written +/// behind it but the hold's line. const USB_RESET_BOOTS: &[metal::Arm] = &[ metal::once( devices::BOOT, @@ -1073,7 +1077,12 @@ const USB_RESET_BOOTS: &[metal::Arm] = &[ metal::once("metalcase", "tests/images/metalcase.toml", &[], &[]), ]; -const METALCASE: &[metal::Arm] = &[metal::once("metalcase", "tests/images/metalcase.toml", &[], &[])]; +/// `tests/images/metalcase.toml`, ended once ps2server's probe has: a boot +/// that ended sooner would cut off what `ps2server_arms` reads. +const METALCASE: &[metal::Arm] = &[metal::Arm { + last: Some("test_rs_ps2server_hold"), + ..metal::once("metalcase", "tests/images/metalcase.toml", &[], &[]) +}]; /// A launcher and a declared `cat` and shell, which `process_tree`'s subtree /// launches, a `toybox` row holding `roster`, which `launch_toctou` races, and From a42489c7cb73c17314b2004d060d4bea46ce7309 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 08:30:54 +0200 Subject: [PATCH 3/8] tests: the clipboard boot's compositor serves input #894's `clipboardcase` landed with a compositor row from before this branch, and the compositor takes the `input` acceptor as it takes `prompt`'s, so it panicked at its start on that boot. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- tests/images/clipboardcase.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/images/clipboardcase.toml b/tests/images/clipboardcase.toml index 703e2d26356..d4db604830d 100644 --- a/tests/images/clipboardcase.toml +++ b/tests/images/clipboardcase.toml @@ -17,7 +17,7 @@ syscap = ["logread"] [programs.compositor] service = true -serves = ["compositor", "prompt", "clipboard"] +serves = ["compositor", "prompt", "clipboard", "input"] devices = ["framebuffer", "keyboard", "mouse"] [programs.vdagent] From 1ca170b2bc179505d718f858d782821c1957a58a Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 09:18:59 +0200 Subject: [PATCH 4/8] ps2server: a loss is scoped to the device that lost something LOST was sent for the keyboard's overrun, for every aux byte the framer discarded and at the pointer's write-off, and the compositor ended both the source's keys and its pointer's buttons on it. So a resync of the TrackPoint released a held Ctrl, and a keyboard overrun released a held button mid-drag. Every PS/2 packet states its buttons whole, so a discarded aux byte owes the owner nothing: it is counted and sends nothing. The write-off owes a report with no buttons for source 0, and no key release. LOST is the keyboard's overrun alone, and the owner ends key state on it; pointer state ends on a zero-buttons report or on the connection's end. The orchestrator's ruling on build review round 1, BLOCKER 2. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- lib/input/src/wire.rs | 3 ++- system/compositor/src/session.rs | 1 - system/ps2server/src/stream.rs | 45 +++++++++++++++++++++++--------- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/lib/input/src/wire.rs b/lib/input/src/wire.rs index ea9282de557..b412ef2a933 100644 --- a/lib/input/src/wire.rs +++ b/lib/input/src/wire.rs @@ -21,7 +21,8 @@ pub const CONNECTIONS: usize = 4; pub const MSG_KEY: u32 = 1; /// A pointer report: one [`MouseEvent`]. pub const MSG_POINTER: u32 = 2; -/// No payload: release everything this source holds. +/// No payload: the source's keyboard lost a transition, so every key it +/// holds is released. Its pointers keep their buttons: a report restates them. pub const MSG_LOST: u32 = 3; /// A key's bytes on the wire. diff --git a/system/compositor/src/session.rs b/system/compositor/src/session.rs index 37b8a4d878e..e0e2c6657c5 100644 --- a/system/compositor/src/session.rs +++ b/system/compositor/src/session.rs @@ -826,7 +826,6 @@ impl Session { Ok(Record::Lost) => { let released: Vec = self.merge.end(slot).collect(); released.into_iter().for_each(|key| self.key(key)); - states.push(self.pointers.end(slot)); } Err(why) => { ended = Some(format!("a record refused: {why:?}")); diff --git a/system/ps2server/src/stream.rs b/system/ps2server/src/stream.rs index 4fa975e68ff..713441eb307 100644 --- a/system/ps2server/src/stream.rs +++ b/system/ps2server/src/stream.rs @@ -21,7 +21,12 @@ //! **A device that resets itself** (`0xAA 0x00`) has its reporting enabled //! again with `0xD4 0xF4` (TP 13), whose `0xFA` comes in the stream within //! TP 46's 25 ms; [`REENABLES`] failures in a row write the pointer off with -//! `0xA7` alone (C 12), so its line never rises again. +//! `0xA7` alone (C 12), so its line never rises again, and its buttons are +//! released by a report holding none. +//! +//! **A loss is the device's that lost something.** The keyboard's overrun +//! code sends `LOST`, which releases keys alone; a pointer byte dropped sends +//! nothing, since every packet states the buttons whole. use toyos_abi::input::{RawKeyEvent, MOD_RELEASED}; use toyos_input::wire::{Motion, Record, Report}; @@ -228,10 +233,10 @@ impl Stream { return; } // Nothing is reported until the enable is out, and nothing after - // the write-off: a byte now is one this stream had to drop. + // the write-off. Aux::Off | Aux::Owed { .. } => { self.counts.discarded += 1; - return out.push(Record::Lost); + return; } } match self.pointer.feed(byte, self.device_ns) { @@ -239,14 +244,10 @@ impl Stream { MouseOutcome::Packet { buttons, dx, dy } => { self.counts.motion += 1; // A 9-bit count, which an `i16` holds. - let motion = Motion::Relative { dx: dx as i16, dy: dy as i16 }; - out.push(Record::Pointer(Report { source: 0, buttons, scroll: 0, motion })); + out.push(pointer(buttons, dx as i16, dy as i16)); } MouseOutcome::Reset => self.aux = Aux::Owed { failures: 0 }, - MouseOutcome::Discarded => { - self.counts.discarded += 1; - out.push(Record::Lost); - } + MouseOutcome::Discarded => self.counts.discarded += 1, } self.aux_run.0.clear(); } @@ -264,7 +265,7 @@ impl Stream { let deadline = c.now() + ACK_NS; if failures >= REENABLES { self.aux = Aux::Off; - out.push(Record::Lost); + out.push(pointer(0, 0, 0)); let sent = self.send(c, Port::Command, DISABLE_AUX, deadline, out, say); say(format!("ps2server: aux enable failed {failures} times — pointer written off ({sent:?})")); return; @@ -313,6 +314,11 @@ impl Stream { } } +/// The one pointer's report: it is source 0 of this connection. +fn pointer(buttons: u8, dx: i16, dy: i16) -> Record { + Record::Pointer(Report { source: 0, buttons, scroll: 0, motion: Motion::Relative { dx, dy } }) +} + #[cfg(test)] mod tests { use toyos_abi::input::MOD_RELEASED; @@ -327,7 +333,7 @@ mod tests { /// `fake` probed and armed, and the stream over it. fn armed(fake: &mut Fake) -> Stream { let mut said = |line: String| eprintln!("{line}"); - let Probed::Armed { aux, backlog, .. } = probe(fake, &mut said) else { panic!("the probe armed nothing") }; + let Probed::Armed { aux, backlog } = probe(fake, &mut said) else { panic!("the probe armed nothing") }; let (stream, records) = Stream::new(fake, aux, &backlog, &mut said); assert!(records.is_empty(), "{records:?}"); stream @@ -408,6 +414,21 @@ mod tests { assert!(said.iter().any(|l| l == "ps2server: aux reset itself, reporting enabled again"), "{said:#?}"); } + /// Shift held, then an aux byte with bit 3 clear, which no packet starts + /// with: the framer drops it, and nothing tells the owner to release a key. + #[test] + fn a_discarded_aux_byte_sends_nothing() { + let mut fake = Fake::qemu(); + let mut stream = armed(&mut fake); + let at = fake.now + MS; + fake.kbd_sends(at, &[0x12]); + fake.aux_sends(at + 5 * MS, &[0x00]); + let (records, _) = run(&mut fake, &mut stream, at + 20 * MS); + let shift = toyos_abi::input::RawKeyEvent { keycode: 0xE1, modifiers: 0 }; + assert_eq!(records, [Record::Key(shift)]); + assert_eq!(stream.counts.discarded, 1, "the byte reached the framer, which dropped it"); + } + #[test] fn three_failed_reenables_write_the_pointer_off_with_0xa7() { let mut fake = Fake::qemu(); @@ -418,7 +439,7 @@ mod tests { let before = fake.commands.len(); let (records, said) = run(&mut fake, &mut stream, at + 200 * MS); assert_eq!(fake.commands[before..], [0xD4, 0xD4, 0xD4, 0xA7], "the enable thrice, then 0xA7 alone"); - assert_eq!(records, [Record::Lost], "the pointer's buttons released"); + assert_eq!(records, [pointer(0, 0, 0)], "the pointer's buttons released, and no key"); assert!(said.iter().any(|l| l.starts_with("ps2server: aux enable failed 3 times — pointer written off")), "{said:#?}"); } From 6a69cce886e3c256598e1fdd4e8a3f5b2444dcd6 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 09:19:08 +0200 Subject: [PATCH 5/8] ps2server, toyos-input: nothing ships for a test alone `Probed::Armed`'s `keyboard` was read only by the probe's tests, which now hold the armed line and the command byte instead; `rel_scale_for` was exported only for `toyos-input`'s integration test. It is private, its aspect test moves into `merge.rs`, and the motion test states its expected scale as the numbers 1920x1080 gives. Build review round 1, BLOCKER 5. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- lib/input/src/lib.rs | 2 +- lib/input/src/merge.rs | 23 +++++++++++++++++- lib/input/tests/input.rs | 24 ++++--------------- system/ps2server/src/main.rs | 2 +- system/ps2server/src/probe.rs | 45 ++++++++++++++++++----------------- 5 files changed, 51 insertions(+), 45 deletions(-) diff --git a/lib/input/src/lib.rs b/lib/input/src/lib.rs index d7186080186..43df3d91171 100644 --- a/lib/input/src/lib.rs +++ b/lib/input/src/lib.rs @@ -20,4 +20,4 @@ pub mod merge; pub mod wire; pub use keys::Keys; -pub use merge::{rel_scale_for, Merge, Pointer, Slot, State}; +pub use merge::{Merge, Pointer, Slot, State}; diff --git a/lib/input/src/merge.rs b/lib/input/src/merge.rs index 6fe95a85d95..c8b85df34e8 100644 --- a/lib/input/src/merge.rs +++ b/lib/input/src/merge.rs @@ -98,7 +98,7 @@ const REL_SCALE: i32 = 64; /// as far across as down: `x * width == y * height`. Per-axis, not a single /// scalar: the absolute space is square and screens are not, so one shared /// scalar would skew diagonal motion by the screen's aspect ratio. -pub fn rel_scale_for(width: u32, height: u32) -> (u16, u16) { +fn rel_scale_for(width: u32, height: u32) -> (u16, u16) { let short = width.min(height) as i32; ( (REL_SCALE * short / width.max(1) as i32).max(1) as u16, @@ -164,3 +164,24 @@ impl Pointer { State { buttons, scroll, x: self.x as u16, y: self.y as u16 } } } + +#[cfg(test)] +mod tests { + use super::rel_scale_for; + + /// Relative motion lands in a square space mapped by width and height, so + /// the counts per pixel differ per axis or the pointer skews by the aspect + /// ratio. + #[test] + fn a_count_moves_as_far_across_as_down() { + for (w, h) in [(1920u32, 1200u32), (1024, 768), (3840, 2160), (1200, 1920), (800, 800)] { + let (sx, sy) = rel_scale_for(w, h); + assert!(sx >= 1 && sy >= 1, "{w}x{h} scaled a count to nothing"); + let across = sx as u64 * w as u64; + let down = sy as u64 * h as u64; + // Each scale is a truncated integer, so each product carries under + // one screen dimension of error. + assert!(across.abs_diff(down) < (w + h) as u64, "{w}x{h} moves {across} across and {down} down per count"); + } + } +} diff --git a/lib/input/tests/input.rs b/lib/input/tests/input.rs index ec59bb57699..809ae04ef53 100644 --- a/lib/input/tests/input.rs +++ b/lib/input/tests/input.rs @@ -2,7 +2,7 @@ use toyos_abi::input::{MouseEvent, RawKeyEvent, MOD_RELEASED, MOD_SHIFT, POINTER_ABSOLUTE, POINTER_RELATIVE}; use toyos_input::wire::{self, Motion, Record, Refused, Report, KEY_LEN, MSG_KEY, MSG_LOST, MSG_POINTER, POINTER_LEN}; -use toyos_input::{rel_scale_for, Merge, Pointer, Slot, State}; +use toyos_input::{Merge, Pointer, Slot, State}; const SHIFT: u8 = 0xE1; const A: u8 = 0x04; @@ -145,13 +145,12 @@ fn a_connections_end_clears_its_pointers_buttons_and_no_others() { #[test] fn relative_motion_scales_by_the_screen_and_absolute_motion_sets() { - let (width, height) = (1920, 1080); - let (sx, sy) = rel_scale_for(width, height); - let mut pointer = Pointer::new(width, height); + // 1080 rows' worth of 64 counts each, so a count is 64 down and 64 * 1080 / 1920 = 36 across. + let mut pointer = Pointer::new(1920, 1080); let start = pointer.report(slot(0), relative(0, 0, 0, 0)); assert_eq!((start.x, start.y), (16384, 16384), "it starts in the middle"); let moved = pointer.report(slot(0), relative(0, 0, 10, -3)); - assert_eq!((moved.x, moved.y), (16384 + 10 * sx, 16384 - 3 * sy)); + assert_eq!((moved.x, moved.y), (16384 + 10 * 36, 16384 - 3 * 64)); let set = pointer.report(slot(1), Report { source: 0, buttons: 0, scroll: 1, motion: Motion::Absolute { x: 7, y: 9 } }); assert_eq!(set, State { buttons: 0, scroll: 1, x: 7, y: 9 }); let clamped = pointer.report(slot(0), relative(0, 0, i16::MIN, i16::MAX)); @@ -160,18 +159,3 @@ fn relative_motion_scales_by_the_screen_and_absolute_motion_sets() { let square = pointer.report(slot(0), relative(0, 0, 1, -1)); assert_eq!((square.x, square.y), (64, 32767 - 64)); } - -/// Relative motion lands in a square space mapped by width and height, so the -/// counts per pixel differ per axis or the pointer skews by the aspect ratio. -#[test] -fn a_count_moves_as_far_across_as_down() { - for (w, h) in [(1920u32, 1200u32), (1024, 768), (3840, 2160), (1200, 1920), (800, 800)] { - let (sx, sy) = rel_scale_for(w, h); - assert!(sx >= 1 && sy >= 1, "{w}x{h} scaled a count to nothing"); - let across = sx as u64 * w as u64; - let down = sy as u64 * h as u64; - // Each scale is a truncated integer, so each product carries under one - // screen dimension of error. - assert!(across.abs_diff(down) < (w + h) as u64, "{w}x{h} moves {across} across and {down} down per count"); - } -} diff --git a/system/ps2server/src/main.rs b/system/ps2server/src/main.rs index 9eb0a762deb..a9645702d46 100644 --- a/system/ps2server/src/main.rs +++ b/system/ps2server/src/main.rs @@ -49,7 +49,7 @@ fn main() { let (aux, backlog) = match probe::probe(&mut hw, &mut say) { Probed::Absent => std::process::exit(0), Probed::Failed => std::process::exit(1), - Probed::Armed { aux, backlog, .. } => (aux, backlog), + Probed::Armed { aux, backlog } => (aux, backlog), }; let (mut stream, records) = stream::Stream::new(&mut hw, aux, &backlog, &mut say); deliver(&mut owner, &records); diff --git a/system/ps2server/src/probe.rs b/system/ps2server/src/probe.rs index ed2d940e359..6858739448f 100644 --- a/system/ps2server/src/probe.rs +++ b/system/ps2server/src/probe.rs @@ -58,9 +58,10 @@ const AUX_STAGE: u8 = CB_AUX_IRQ | CB_SYSTEM_FLAG | CB_KEYBOARD_DISABLED | CB_TR pub enum Probed { /// Nothing decodes the controller's ports. Absent, - /// The keyboard, the pointer or both scan. `backlog` is every byte read + /// The keyboard, the pointer or both scan, and the armed line says which; + /// `aux` is whether the pointer is served. `backlog` is every byte read /// since they did, in order, for the stream's decoders first. - Armed { keyboard: bool, aux: bool, backlog: Vec }, + Armed { aux: bool, backlog: Vec }, /// Neither is served, and each was said by name. Failed, } @@ -103,7 +104,7 @@ impl Probe<'_, C> { return Err(Stopped); } self.arm(keyboard, aux)?; - Ok(Probed::Armed { keyboard, aux, backlog: core::mem::take(&mut self.backlog) }) + Ok(Probed::Armed { aux, backlog: core::mem::take(&mut self.backlog) }) } fn deadline(&mut self, ns: u64) -> u64 { @@ -342,9 +343,9 @@ mod tests { (probed, said) } - fn armed(fake: &mut Fake) -> (bool, bool, Vec, Vec) { + fn armed(fake: &mut Fake) -> (bool, Vec, Vec) { match probed(fake) { - (Probed::Armed { keyboard, aux, backlog }, said) => (keyboard, aux, backlog, said), + (Probed::Armed { aux, backlog }, said) => (aux, backlog, said), (other, said) => panic!("probed {other:?}: {said:#?}"), } } @@ -383,8 +384,8 @@ mod tests { #[test] fn qemu_arms_its_keyboard_set_2_translated_and_its_mouse() { let mut fake = Fake::qemu(); - let (keyboard, aux, backlog, said) = armed(&mut fake); - assert!(keyboard && aux, "{said:#?}"); + let (aux, backlog, said) = armed(&mut fake); + assert!(aux, "{said:#?}"); assert_eq!(fake.cb, 0x47, "both interrupts, the System Flag, translation, both clocks on"); line(&said, "ps2server: armed, command byte 0x57 — keyboard on, aux on"); line(&said, "ps2server: aux reset"); @@ -396,20 +397,21 @@ mod tests { #[test] fn the_t14_arms_its_keyboard_and_its_trackpoint() { let mut fake = Fake::t14(); - let (keyboard, aux, _, said) = armed(&mut fake); - assert!(keyboard && aux, "{said:#?}"); + let (aux, _, said) = armed(&mut fake); + assert!(aux, "{said:#?}"); let (acked, passed) = reset_times(&said); assert!((80..100).contains(&acked), "the 0xfa {acked} us after the reset"); assert!((516_000..516_100).contains(&passed), "the 0xaa {passed} us after the 0xfa"); assert!(line(&said, "ps2server: aux reset").contains("0xaa 367"), "{said:#?}"); assert_eq!(fake.cb, 0x47); + line(&said, "ps2server: armed, command byte 0x57 — keyboard on, aux on"); } #[test] fn the_amd_bench_arms_its_keyboard_and_finds_no_pointer() { let mut fake = Fake::amd(); - let (keyboard, aux, backlog, said) = armed(&mut fake); - assert!(keyboard && !aux, "{said:#?}"); + let (aux, backlog, said) = armed(&mut fake); + assert!(!aux, "{said:#?}"); let (_, passed) = reset_times(&said); assert!((2_200..2_300).contains(&passed), "the 0xaa {passed} us after the 0xfa"); line(&said, "ps2server: aux absent — its reset went unanswered in 600 ms"); @@ -429,8 +431,8 @@ mod tests { // The keyboard stage ends 516 ms in; the aux device answers its reset // 369.5 ms after that. fake.type_a(700 * MS); - let (keyboard, aux, backlog, said) = armed(&mut fake); - assert!(keyboard && aux, "{said:#?}"); + let (aux, backlog, said) = armed(&mut fake); + assert!(aux, "{said:#?}"); assert!(!said.iter().any(|l| l.contains("drained")), "{said:#?}"); let until = fake.now + 10 * MS; let records = records(&mut fake, aux, &backlog, until); @@ -446,8 +448,8 @@ mod tests { let mut fake = Fake::t14(); fake.cb = 0x07; fake.kbd.set = 1; - let (keyboard, aux, backlog, said) = armed(&mut fake); - assert!(keyboard && aux, "{said:#?}"); + let (aux, backlog, said) = armed(&mut fake); + assert!(aux, "{said:#?}"); let at = fake.now + MS; fake.type_a(at); assert!(typed_a(&records(&mut fake, aux, &backlog, at + 10 * MS))); @@ -457,18 +459,18 @@ mod tests { fn a_reset_answered_0xfc_disables_the_keyboard_by_name() { let mut fake = Fake::t14(); fake.kbd.reset = Reset { ack: Some(80 * US), then: Some((516 * MS, 0xFC)) }; - let (keyboard, aux, _, said) = armed(&mut fake); - assert!(!keyboard && aux, "{said:#?}"); + let (aux, _, said) = armed(&mut fake); + assert!(aux, "{said:#?}"); line(&said, "ps2server: kbd DISABLED — the reset's 0xfa was followed by 0xfc, not 0xaa"); assert_eq!(fake.cb, 0x56, "the keyboard's interrupt off and its interface disabled"); + line(&said, "ps2server: armed, command byte 0x56 — keyboard off, aux on"); } #[test] fn an_0xaa_past_the_keyboard_stages_ceiling_disables_the_keyboard_by_name() { let mut fake = Fake::t14(); fake.kbd.reset = Reset::answered(80 * US, KEYBOARD_NS + MS); - let (keyboard, _, _, said) = armed(&mut fake); - assert!(!keyboard, "{said:#?}"); + let (_, _, said) = armed(&mut fake); line(&said, "ps2server: kbd DISABLED — no 0xaa in 750 ms of the reset (the output buffer empty)"); } @@ -476,8 +478,7 @@ mod tests { fn a_reset_with_no_0xfa_disables_the_keyboard_by_name() { let mut fake = Fake::t14(); fake.kbd.reset = Reset { ack: None, then: None }; - let (keyboard, _, _, said) = armed(&mut fake); - assert!(!keyboard, "{said:#?}"); + let (_, _, said) = armed(&mut fake); line(&said, "ps2server: kbd DISABLED — no 0xfa to the reset in 750 ms (the output buffer empty)"); } @@ -506,7 +507,7 @@ mod tests { fn bytes_waiting_before_the_probe_are_drained_by_name() { let mut fake = Fake::t14(); fake.kbd_sends(0, &[0x1C, 0x1C]); - let (_, _, backlog, said) = armed(&mut fake); + let (_, backlog, said) = armed(&mut fake); assert!(backlog.is_empty(), "{backlog:?}"); assert!(said.iter().any(|l| l.starts_with("ps2server: drained") && l.contains("0x1e")), "{said:#?}"); } From fe2f1e3a55cd93d621a9f379172779ff23077922 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 09:20:19 +0200 Subject: [PATCH 6/8] ps2server: a wedged controller is found before the probe writes, and the deadline's look is tested The quiet stage wrote 0xAD first, and a wedged output buffer kept that write reading for its whole 250 ms, about 35,000 reads, ending "never took 0xAD" with every byte it read unsaid. It now drains before its first write as after, so a buffer still full after 32 reads is said with what they read, and no write is made into it (C 14). A write the controller never takes says what it drained first. Hw's wait reads the clock before it looks for a record, so a record landing as the deadline passes is still found. That loop is now `Records::first`, and a host test preempts it between every two steps: read the clock after the look and it reds. Build review round 1, NOTEs 4 and 5. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- system/ps2server/src/bus.rs | 70 +++++++++++++++++++++++++++++++++++ system/ps2server/src/hw.rs | 46 +++++++++++------------ system/ps2server/src/probe.rs | 32 ++++++++++++---- 3 files changed, 118 insertions(+), 30 deletions(-) diff --git a/system/ps2server/src/bus.rs b/system/ps2server/src/bus.rs index 598ed4e9c75..48df57ba406 100644 --- a/system/ps2server/src/bus.rs +++ b/system/ps2server/src/bus.rs @@ -140,3 +140,73 @@ pub trait Bus: Controller { } } } + +/// A claim's interrupt records, as a [`Controller::wait`] reaches them. +pub trait Records { + /// The clock the records are stamped with. + fn clock(&mut self) -> u64; + /// The record the claim holds, if any, taken. + fn take(&mut self) -> Option; + /// Sleep until a record may have landed, or `deadline`; `now` is the clock + /// read before the last [`Self::take`]. + fn park(&mut self, now: u64, deadline: u64); + + /// A record, waited for until `deadline`: the clock is read before the + /// look, so a record that lands after the deadline's read is still found. + fn first(&mut self, deadline: u64) -> Option { + loop { + let now = self.clock(); + if let Some(irq) = self.take() { + return Some(irq); + } + if now >= deadline { + return None; + } + self.park(now, deadline); + } + } +} + +#[cfg(test)] +mod tests { + use super::{Irq, Records}; + + /// One instant per call, as a waiter preempted between every two of its + /// steps sees the machine; the record lands at [`Self::LANDS`]. + struct Preempted { + at: u64, + } + + impl Preempted { + const LANDS: u64 = 4; + + fn step(&mut self) -> u64 { + let at = self.at; + self.at += 1; + at + } + } + + impl Records for Preempted { + fn clock(&mut self) -> u64 { + self.step() + } + + fn take(&mut self) -> Option { + (self.step() >= Self::LANDS).then_some(Irq { first_nanos: Self::LANDS }) + } + + fn park(&mut self, _: u64, _: u64) { + self.step(); + } + } + + /// The deadline passes between the waiter's look and its clock read, and + /// the record lands with it: a waiter that read the clock after its look + /// would call that silence. + #[test] + fn a_record_landing_as_the_deadline_passes_is_found() { + let mut records = Preempted { at: 0 }; + assert_eq!(records.first(Preempted::LANDS), Some(Irq { first_nanos: Preempted::LANDS })); + } +} diff --git a/system/ps2server/src/hw.rs b/system/ps2server/src/hw.rs index 2c3f79410ef..65aebd9510c 100644 --- a/system/ps2server/src/hw.rs +++ b/system/ps2server/src/hw.rs @@ -7,7 +7,7 @@ use toyos_abi::ioport; use toyos_abi::pci::DeviceIrqRecord; use toyos_abi::syscall::{self, SyscallError}; -use crate::bus::{Controller, Irq}; +use crate::bus::{Controller, Irq, Records}; const DATA: u16 = 0x60; const STATUS: u16 = 0x64; @@ -28,9 +28,15 @@ impl Hw { assert_eq!(n, description.len(), "ps2server: a description of {n} bytes"); Self { claim, poller: Poller::new(1), watching: false } } +} + +impl Records for Hw { + fn clock(&mut self) -> u64 { + toyos_abi::clock::nanos_since_boot() + } /// The claim's record, if it holds one. - fn record(&self) -> Option { + fn take(&mut self) -> Option { let mut record = [0u8; DeviceIrqRecord::SIZE]; match syscall::read_nonblock(self.claim.as_handle(), &mut record) { Ok(n) => { @@ -42,6 +48,19 @@ impl Hw { Err(other) => panic!("ps2server: a bound claim's read answered {other:?}"), } } + + fn park(&mut self, now: u64, deadline: u64) { + if !self.watching { + self.poller.watch(&self.claim, READABLE, 0); + self.watching = true; + } + let timeout = if deadline == u64::MAX { u64::MAX } else { deadline - now }; + let mut fired = false; + self.poller.wait(1, timeout, |_| fired = true); + if fired { + self.watching = false; + } + } } impl Controller for Hw { @@ -62,29 +81,10 @@ impl Controller for Hw { } fn now(&mut self) -> u64 { - toyos_abi::clock::nanos_since_boot() + self.clock() } fn wait(&mut self, deadline: u64) -> Option { - loop { - let now = self.now(); - let past = now >= deadline; - if let Some(irq) = self.record() { - return Some(irq); - } - if past { - return None; - } - if !self.watching { - self.poller.watch(&self.claim, READABLE, 0); - self.watching = true; - } - let timeout = if deadline == u64::MAX { u64::MAX } else { deadline - now }; - let mut fired = false; - self.poller.wait(1, timeout, |_| fired = true); - if fired { - self.watching = false; - } - } + self.first(deadline) } } diff --git a/system/ps2server/src/probe.rs b/system/ps2server/src/probe.rs index 6858739448f..d7849fbc6bf 100644 --- a/system/ps2server/src/probe.rs +++ b/system/ps2server/src/probe.rs @@ -6,7 +6,8 @@ //! each by its own page; **TP** is IBM's *TrackPoint System Version 4.0 //! Engineering Specification*, the auxiliary device's protocol. //! -//! 1. **Quiet:** `0xAD`, `0xA7` (C 12), and the output buffer drained. +//! 1. **Quiet:** the output buffer drained, `0xAD`, `0xA7` (C 12), and it +//! drained again. //! 2. **Keyboard:** the command byte with the keyboard's interrupt alone, //! translation on and both interfaces still disabled; `0xAE`; then IBM's //! Reset, `0xFF` (K 13), answered `0xFA` and the completion code `0xAA`, @@ -38,7 +39,7 @@ pub const KEYBOARD_NS: u64 = 750_000_000; /// The aux stage's: TP 9's 500 ms of diagnostics, and its acknowledgements. const AUX_NS: u64 = 600_000_000; -/// The most the quiet stage reads before it calls the output buffer wedged. +/// The most one drain reads before it calls the output buffer wedged. const DRAIN: usize = 32; /// The keyboard's Reset, its acknowledgement and its completion code (K 13). @@ -141,6 +142,7 @@ impl Probe<'_, C> { self.route(b); } sent.map_err(|NotTaken { status }| { + self.said_drained(&format!("waiting to write {what}")); (self.say)(format!("ps2server: the controller never took {what} (status {status:#04x})")); Stopped }) @@ -173,18 +175,23 @@ impl Probe<'_, C> { } } - /// Step 1. + /// Step 1, drained before its first write too, so a wedged output buffer + /// is found by [`DRAIN`] reads and no write is made into it (C 14). fn quiet(&mut self) -> Result<(), Stopped> { + self.drain()?; let deadline = self.deadline(CONTROLLER_NS); self.command(DISABLE_KEYBOARD, deadline, "0xAD")?; self.command(DISABLE_AUX, deadline, "0xA7")?; + self.drain()?; + self.said_drained("before the probe"); + Ok(()) + } + + fn drain(&mut self) -> Result<(), Stopped> { for _ in 0..DRAIN { match self.c.poll() { Some(byte) => self.route(byte), - None => { - self.said_drained("before the probe"); - return Ok(()); - } + None => return Ok(()), } } self.said_drained("before the probe"); @@ -503,6 +510,17 @@ mod tests { line(&said, "ps2server: DISABLED — command byte 0x57 did not take (read back 0x56)"); } + #[test] + fn a_wedged_output_buffer_is_said_after_32_reads_with_nothing_written() { + let mut fake = Fake::t14(); + fake.wedged = true; + let (probed, said) = probed(&mut fake); + assert_eq!(probed, Probed::Failed); + line(&said, "ps2server: drained 32 byte(s) before the probe: [0x55, 0x55"); + line(&said, "ps2server: the output buffer never drains — 32 reads left it full"); + assert!(fake.commands.is_empty(), "C 14: a write into a full output buffer: {:02x?}", fake.commands); + } + #[test] fn bytes_waiting_before_the_probe_are_drained_by_name() { let mut fake = Fake::t14(); From 3fa242c112416380e81987736eb98f4f91cd126e Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 09:22:29 +0200 Subject: [PATCH 7/8] kernel: a USB pointer's source number and its five buttons are decided where host tests reach them Deleting `test-input-merge` cut the only test of `PointerSource`'s allocation, which stays in the shipping kernel: a `unbind` that gave no number back would leak one of 256 on every replug, and nothing went red. The allocation is `kernel::pointers::Sources` in the pure library now, host-tested: two claims take distinct numbers, a number given back is the next claim's, all 256 held refuse a claim, and a number given back twice panics. `mouse.rs` keeps the type and calls it. The mask to boot-mouse order's five buttons moves from `mouse::report` into `toyos-usbhid`'s decode, reading `toyos_input::wire::BUTTONS`, the one declaration the owner's decode refuses past; its test is there. Build review round 1, BLOCKER 3 and NOTE 3. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- kernel/pure/lib.rs | 4 +- kernel/pure/pointers.rs | 85 ++++++++++++++++++++++++++++++++++++ kernel/src/mouse.rs | 33 +++----------- kernel/usbhid/src/pointer.rs | 22 ++++++++-- lib/input/src/wire.rs | 2 +- 5 files changed, 114 insertions(+), 32 deletions(-) create mode 100644 kernel/pure/pointers.rs diff --git a/kernel/pure/lib.rs b/kernel/pure/lib.rs index 699b097296b..f2ca5bdb127 100644 --- a/kernel/pure/lib.rs +++ b/kernel/pure/lib.rs @@ -4,7 +4,8 @@ //! range ([`mtrr`]), what a pipe's ends are told of each other ([`pipe`]), what //! a CPU waiting on the boot processor's write to `SMI_CMD` decides //! ([`bootwrite`]), what the clock computes from counter ticks ([`clock`]), -//! which number a kernel volume's directory answers ([`dirnodes`]) and which +//! which number a kernel volume's directory answers ([`dirnodes`]), which +//! number a USB pointer's records carry ([`pointers`]) and which //! state components a CPU's `XCR0` enables ([`xsave`]). //! The kernel binary links it; the host runs //! its tests, because none of it reads a register, a clock or a kernel lock. @@ -22,6 +23,7 @@ pub mod dirnodes; pub mod mtrr; pub mod pcid; pub mod pipe; +pub mod pointers; pub mod proclife; pub mod sched; pub mod xsave; diff --git a/kernel/pure/pointers.rs b/kernel/pure/pointers.rs new file mode 100644 index 00000000000..0aa56bd9fbc --- /dev/null +++ b/kernel/pure/pointers.rs @@ -0,0 +1,85 @@ +//! Which number a USB pointer's records carry as theirs: the lowest no bound +//! pointer holds, given back when the pointer goes. Two live pointers never +//! share one, or the owner of input would OR one's buttons into the other's, +//! and a replug takes a freed number again, so a `u8` never runs out. + +use core::sync::atomic::{AtomicU64, Ordering}; + +/// One bit per number a `u8` names, set while a pointer holds it. +pub struct Sources([AtomicU64; 4]); + +impl Sources { + pub const fn new() -> Self { + Self([const { AtomicU64::new(0) }; 4]) + } + + /// The lowest free number, taken; `None` while all 256 are held. + pub fn claim(&self) -> Option { + for (word, bits) in self.0.iter().enumerate() { + let mut seen = bits.load(Ordering::Relaxed); + while seen != u64::MAX { + let bit = seen.trailing_ones(); + match bits.compare_exchange_weak(seen, seen | (1 << bit), Ordering::Relaxed, Ordering::Relaxed) { + Ok(_) => return Some((word as u32 * 64 + bit) as u8), + Err(now) => seen = now, + } + } + } + None + } + + /// `source`, which a claim took, is free again. + pub fn give_back(&self, source: u8) { + let bit = 1u64 << (source % 64); + let was = self.0[usize::from(source) / 64].fetch_and(!bit, Ordering::Relaxed); + assert!(was & bit != 0, "pointer source {source} given back while free"); + } +} + +impl Default for Sources { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::Sources; + use std::vec::Vec; + + #[test] + fn two_claims_take_distinct_numbers() { + let sources = Sources::new(); + assert_eq!((sources.claim(), sources.claim()), (Some(0), Some(1))); + } + + #[test] + fn a_number_given_back_is_the_next_claims() { + let sources = Sources::new(); + let held: Vec = (0..=200).map(|_| sources.claim().expect("a free number")).collect(); + sources.give_back(held[0]); + sources.give_back(held[200]); + assert_eq!(sources.claim(), Some(0)); + assert_eq!(sources.claim(), Some(200)); + assert_eq!(sources.claim(), Some(201)); + } + + #[test] + fn every_number_held_refuses_a_claim_until_one_is_given_back() { + let sources = Sources::new(); + let held: Vec = (0..256).map(|_| sources.claim().expect("a free number")).collect(); + assert_eq!(held, (0..=255).collect::>()); + assert_eq!(sources.claim(), None); + sources.give_back(130); + assert_eq!(sources.claim(), Some(130)); + } + + #[test] + #[should_panic(expected = "pointer source 7 given back while free")] + fn a_number_given_back_twice_panics() { + let sources = Sources::new(); + (0..8).for_each(|_| assert!(sources.claim().is_some())); + sources.give_back(7); + sources.give_back(7); + } +} diff --git a/kernel/src/mouse.rs b/kernel/src/mouse.rs index 1d6fa3eaeb0..3e977b701d2 100644 --- a/kernel/src/mouse.rs +++ b/kernel/src/mouse.rs @@ -7,9 +7,9 @@ //! pointer's buttons and keeps the one position (`toyos_input::Pointer`). use alloc::collections::VecDeque; -use core::sync::atomic::{AtomicU64, Ordering}; use crate::sync::Lock; use crate::watch::Watch; +use kernel::pointers::Sources; pub use toyos_abi::input::MouseEvent; use toyos_abi::input::{POINTER_ABSOLUTE, POINTER_RELATIVE}; @@ -20,36 +20,16 @@ pub const MAX_QUEUED_EVENTS: usize = 512; /// What a pointer poll waits on; no read blocks on the mouse. pub static WATCH: Watch = Watch::new(); -/// The buttons a record carries: boot-mouse order's five, which are what the -/// owner merges. -const BUTTONS: u8 = 0x1F; - /// Which physical pointer a report came from, keyed by device and not by bus. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct PointerSource(u8); -/// One bit per source a [`PointerSource`] can name. -static IN_USE: [AtomicU64; 4] = [const { AtomicU64::new(0) }; 4]; +static IN_USE: Sources = Sources::new(); impl PointerSource { /// The lowest free source, or `None` when every one is held; a caller that gets `None` must not bind the device, or two devices' buttons share one record. pub fn claim() -> Option { - for (word, bits) in IN_USE.iter().enumerate() { - let mut seen = bits.load(Ordering::Relaxed); - while seen != u64::MAX { - let bit = seen.trailing_ones(); - match bits.compare_exchange_weak( - seen, - seen | (1 << bit), - Ordering::Relaxed, - Ordering::Relaxed, - ) { - Ok(_) => return Some(Self((word as u32 * 64 + bit) as u8)), - Err(now) => seen = now, - } - } - } - None + IN_USE.claim().map(Self) } /// What the records of this source carry as theirs. @@ -66,7 +46,8 @@ pub enum Motion { Absolute { x: u16, y: u16 }, } -/// Queue one report of `source`'s. +/// Queue one report of `source`'s; `buttons` as `toyos_usbhid` decodes them, +/// boot-mouse order's five. pub fn report(source: PointerSource, buttons: u8, motion: Motion, scroll: i8) { let (kind, x, y) = match motion { Motion::Relative { dx, dy } => (POINTER_RELATIVE, dx, dy), @@ -77,7 +58,7 @@ pub fn report(source: PointerSource, buttons: u8, motion: Motion, scroll: i8) { if buf.len() >= MAX_QUEUED_EVENTS { buf.pop_front(); } - buf.push_back(MouseEvent { source: source.0, buttons: buttons & BUTTONS, scroll, kind, x, y }); + buf.push_back(MouseEvent { source: source.0, buttons, scroll, kind, x, y }); } /// Throw away everything queued, for the reason [`crate::keyboard::discard_queued`] gives. @@ -94,7 +75,7 @@ pub fn release_buttons(source: PointerSource) { pub fn unbind(source: PointerSource) { release_buttons(source); // Released before the source frees, or the next device's records follow a stale button. - IN_USE[source.0 as usize / 64].fetch_and(!(1u64 << (source.0 % 64)), Ordering::Relaxed); + IN_USE.give_back(source.0); } pub fn has_data() -> bool { diff --git a/kernel/usbhid/src/pointer.rs b/kernel/usbhid/src/pointer.rs index 829e16bcb41..a3f0c4f2c14 100644 --- a/kernel/usbhid/src/pointer.rs +++ b/kernel/usbhid/src/pointer.rs @@ -9,14 +9,16 @@ //! byte 5 the wheel //! ``` //! -//! Buttons are carried whole: the bits past the boot three are further -//! buttons on every pointer this decoder reads, QEMU's included. +//! Buttons are boot-mouse order's five, which is what an owner of input +//! merges (`toyos_input::wire::BUTTONS`): the bits past them are dropped. //! //! Appendix B.2 lays out bytes 0 to 2 and leaves the rest to the device. //! Byte 3 is read as the wheel because QEMU's `usb-mouse` report descriptor //! puts it there and Linux's boot mouse driver `usbmouse` reads it so; a //! mouse that puts something else there scrolls. +use toyos_input::wire::BUTTONS; + /// The largest absolute coordinate, which is the tablet's logical maximum and /// the edge of the space every pointer's motion is merged into. pub const MOST: u16 = 0x7FFF; @@ -64,6 +66,11 @@ impl Pointer { /// `report`, as this layout reads it, or why it does not. pub fn decode(self, report: &[u8]) -> Result { + let read = self.layout(report)?; + Ok(Report { buttons: read.buttons & BUTTONS, ..read }) + } + + fn layout(self, report: &[u8]) -> Result { match (self, report) { (Self::Mouse, &[buttons, dx, dy]) => Ok(Report { buttons, @@ -108,7 +115,14 @@ mod tests { fn a_mouse_report_is_read_as_appendix_b_lays_it_out() { assert_eq!(Pointer::Mouse.decode(&[0x01, 0x05, 0xFB]), mouse(0x01, 5, -5, 0)); assert_eq!(Pointer::Mouse.decode(&[0x07, 0x80, 0x7F, 0xFF]), mouse(0x07, -128, 127, -1)); - assert_eq!(Pointer::Mouse.decode(&[0xF8, 0, 0, 1]), mouse(0xF8, 0, 0, 1), "every button bit is carried"); + } + + /// Every record an owner of input reads names boot-mouse order's five + /// buttons at most, so one a mouse sends past them decodes. + #[test] + fn a_button_past_the_fifth_is_dropped() { + assert_eq!(Pointer::Mouse.decode(&[0xF8, 0, 0, 1]), mouse(0x18, 0, 0, 1)); + assert_eq!(Pointer::Tablet.decode(&[0xE1, 0, 0, 0, 0, 0]), tablet(0x01, 0, 0, 0)); } #[test] @@ -185,7 +199,7 @@ mod tests { match pointer.decode(&report) { Ok(read) => { assert!(len <= pointer.request(), "case {case}: {pointer:?} read {len} bytes"); - assert_eq!(read.buttons, report[0], "case {case}"); + assert_eq!(read.buttons, report[0] & BUTTONS, "case {case}"); match read.motion { Motion::Relative { dx, dy } => { assert_eq!(pointer, Pointer::Mouse, "case {case}"); diff --git a/lib/input/src/wire.rs b/lib/input/src/wire.rs index b412ef2a933..cea288cacb0 100644 --- a/lib/input/src/wire.rs +++ b/lib/input/src/wire.rs @@ -34,7 +34,7 @@ pub const POINTER_LEN: usize = core::mem::size_of::(); pub const KEEP: usize = POINTER_LEN + 1; /// The buttons boot-mouse order names. -const BUTTONS: u8 = 0x1F; +pub const BUTTONS: u8 = 0x1F; /// Where a pointer report puts the pointer. #[derive(Clone, Copy, PartialEq, Eq, Debug)] From 26c86737400f3353287e319b232b984c1c117797 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 09:23:55 +0200 Subject: [PATCH 8/8] isa: the claim's refusal of a row with a port the kernel declared has a test again Deleting `isa_claim_refused` left `claim_row`'s `KernelDriven` arm with no test, and the arm is reachable: a FADT reset register at 0x64 is declared to the kernel (`power::init_reset`), and then the i8042's row must not be handed out. The `isa-port-declared` actuator declares 0x64 to the kernel at boot, and the `isa_claim_refused_on_a_kernel_port` T14 row runs `test_rs_isa_claim_refused` under it: the claim answers PermissionDenied, and the kernel names the port and its holder. Build review round 1, BLOCKER 4. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- kernel/src/actuator.rs | 5 ++++ kernel/src/arch/x86_64/pio.rs | 3 +++ tests/common/isa.rs | 6 +++++ .../rust-tests/src/bin/isa_claim_refused.rs | 25 +++++++++++++++++++ tests/metal/src/lib.rs | 4 +++ tests/toyos.rs | 21 ++++++++++++++++ 6 files changed, 64 insertions(+) create mode 100644 tests/guest/rust-tests/src/bin/isa_claim_refused.rs diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index 385f08053e9..f61d1cd649b 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -52,6 +52,11 @@ actuators! { /// AMD `SYSRET` SS-attributes workaround's only guest-observable proof. sysret_ss_probe = "sysret-ss-probe"; + /// Declare the i8042's command port to the kernel at boot, as a FADT + /// reset register at 0x64 is, so an `isa` claim on its row is refused by + /// name. Judged by `isa_claim_refused_on_a_kernel_port`. + isa_port_declared = "isa-port-declared"; + /// Run the xHCI extended-capability walk over eight malformed lists at init. xhci_xecp_selftest = "xhci-xecp-selftest"; diff --git a/kernel/src/arch/x86_64/pio.rs b/kernel/src/arch/x86_64/pio.rs index 320d23a3426..3727f6edbcb 100644 --- a/kernel/src/arch/x86_64/pio.rs +++ b/kernel/src/arch/x86_64/pio.rs @@ -254,4 +254,7 @@ pub fn fill_i8042_row() { wires, }, ); + if crate::actuator::isa_port_declared() { + declare("the isa-port-declared actuator", Ports::one(0x64), Mediated::Kept).expect("isa: 0x64 declared before the actuator"); + } } diff --git a/tests/common/isa.rs b/tests/common/isa.rs index 44b13a8ecff..6b2b9278809 100644 --- a/tests/common/isa.rs +++ b/tests/common/isa.rs @@ -40,3 +40,9 @@ pub fn ports(kernel: &Serial) -> Result<(), String> { pub fn lines(kernel: &Serial) -> Result<(), String> { said(kernel, "isa: the i8042 took its first interrupt", 1) } + +/// `test_rs_isa_claim_refused` under `isa-port-declared`: the claim was +/// refused naming the port the kernel declared and who holds it. +pub fn refused(kernel: &Serial) -> Result<(), String> { + kernel.must_say("isa: the i8042's ports 0x64+1 are the isa-port-declared actuator's").map(drop) +} diff --git a/tests/guest/rust-tests/src/bin/isa_claim_refused.rs b/tests/guest/rust-tests/src/bin/isa_claim_refused.rs new file mode 100644 index 00000000000..3113fc81abc --- /dev/null +++ b/tests/guest/rust-tests/src/bin/isa_claim_refused.rs @@ -0,0 +1,25 @@ +//! Where the kernel declared a port of the i8042's row, an `isa` claim on the +//! row is refused: a claim may not hand a process a port the kernel drives. +//! +//! Run under `isa-port-declared`, which declares the command port to the +//! kernel as a FADT reset register at 0x64 is. + +use toyos::endow::Endowments; +use toyos::syscap::SysCap; +use toyos::Device; +use toyos_abi::syscall::{IsaId, SyscallError, SYSCAP_LABEL}; + +fn main() { + let cap: SysCap = Endowments::get() + .take(SYSCAP_LABEL) + .expect("the test estate is endowed a device-minting capability"); + let set = IsaId::parse("0060,0064:1,12").expect("the i8042's set"); + match cap.claim_isa::(set) { + Err(SyscallError::PermissionDenied) => {} + other => panic!( + "a claim on a row whose port the kernel declared answered {:?}, not PermissionDenied", + other.map(|_| ()) + ), + } + println!("the claim on a row with a port the kernel declared was refused PermissionDenied"); +} diff --git a/tests/metal/src/lib.rs b/tests/metal/src/lib.rs index dd8b0b568ea..e74f495f923 100644 --- a/tests/metal/src/lib.rs +++ b/tests/metal/src/lib.rs @@ -755,6 +755,10 @@ pub const FLASHABLE: &[&str] = &[ // A probe rather than staged inputs, and a read: it runs inside the first // syscall's own context switch. "sysret-ss-probe", + // It enters one port in the kernel's table of declared ports at boot. No + // kernel path reads or writes a port for that entry, so it reaches no + // device; what it changes is that a claim on the i8042's row is refused. + "isa-port-declared", // Three nested `scheduler::Operation`s with known deadlines, in both homes, // each printing what it asked for and what it observed. It establishes and // drops them and reaches nothing else. diff --git a/tests/toyos.rs b/tests/toyos.rs index 4add4a88153..bc9287762d7 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -113,6 +113,10 @@ const RUST_SKIP: &[&str] = &[ // metal rows run them. "isa_grant", "isa_lines", + // It needs a kernel that declared a port of the i8042's row, which only + // the `isa-port-declared` actuator makes: the + // `isa_claim_refused_on_a_kernel_port` metal row runs it. + "isa_claim_refused", // It claims the T14's I219, which no guest has: the // `claim_reuses_its_remapping_entry` and `claim_refused_without_remapping` // metal rows run it. @@ -991,6 +995,23 @@ const METAL: &[(&str, metal::Metal)] = &[ }, }, ), + ( + // A port of the row the kernel declared, as a FADT reset register at + // 0x64 is: the claim is refused naming who holds it. + "isa_claim_refused_on_a_kernel_port", + metal::Metal { + arms: &[metal::once( + "isa-port-declared", + "tests/images/testcases.toml", + &["isa-port-declared"], + &["test_rs_isa_claim_refused"], + )], + judge: |b| { + b[0].job_passed("test_rs_isa_claim_refused")?; + isa::refused(&b[0].kernel()) + }, + }, + ), ( "crash_report_reads_no_kernel_memory", metal::Metal {