diff --git a/Cargo.lock b/Cargo.lock index 126652cbcee..762491ce551 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", ] @@ -1834,7 +1836,6 @@ dependencies = [ "toyos-microcode", "toyos-pci", "toyos-phys", - "toyos-ps2", "toyos-quiesce", "toyos-random", "toyos-rootimage", @@ -2926,6 +2927,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" @@ -4108,6 +4119,7 @@ dependencies = [ "toyos-fat32", "toyos-fat32-check", "toyos-gpt", + "toyos-input", "toyos-keymap", "toyos-logstream", "toyos-manifest", @@ -4138,6 +4150,7 @@ name = "toyos-desktop" version = "0.1.0" dependencies = [ "toyos-abi", + "toyos-input", ] [[package]] @@ -4216,6 +4229,13 @@ dependencies = [ "toyos-device-memory", ] +[[package]] +name = "toyos-input" +version = "0.1.0" +dependencies = [ + "toyos-abi", +] + [[package]] name = "toyos-inspect" version = "0.1.0" @@ -4500,6 +4520,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 6f22d0e590c..d1205df4082 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,6 @@ members = [ "kernel/microcode", "kernel/pci", "kernel/phys", - "kernel/ps2", "kernel/sim", "kernel/smmu", "kernel/usbhid", @@ -53,6 +52,7 @@ members = [ "lib/fat32/check", "lib/gpt", "lib/hda", + "lib/input", "lib/inspect", "lib/logkeeper-api", "lib/logstream", @@ -113,6 +113,8 @@ members = [ "system/netstack/udp", "system/netstack/wire", "system/pkg", + "system/ps2server", + "system/ps2server/ps2", "system/shell", "system/soundserver", "system/soundserver/mixer", @@ -211,6 +213,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 5b009f2233e..bae5c949442 100644 --- a/build/build.rs +++ b/build/build.rs @@ -597,12 +597,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, the prompt port, -/// whose holder draws above every window and has every key while it does, and -/// the clipboard port, whose holder is sent every copy made in the session. +/// whose holder draws above every window and has every key while it does, the +/// input port, whose holder types into the focused window, and the clipboard +/// port, whose holder is sent every copy made in the session. 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), (toyos_manifest::clipboard::PORT, toyos_manifest::clipboard::HOLDER), ]; @@ -3143,6 +3145,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 8949e71ce82..a4d35efc5aa 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", "vdagent"] +start = ["logkeeper", "diskserver", "fileserver", "compositor", "soundserver", "netstack", "filepicker", "acpiserver", "vdagent", "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 @@ -70,15 +70,26 @@ receives = ["power"] # 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. `clipboard` is sent every copy -# made in the session, and `vdagent` alone receives it. +# made in the session, and `vdagent` alone receives it. `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", "clipboard"] +serves = ["compositor", "prompt", "clipboard", "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 764d97701c8..3fac3f592d1 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 @@ -300,7 +299,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 @@ -314,7 +313,7 @@ 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, delivering to the owner @@ -337,7 +336,7 @@ Beside that order, each waiting on something outside it: 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 ef5a3dcd907..e0d4e77b46e 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -496,7 +496,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/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/actuator.rs b/kernel/src/actuator.rs index 2420be035be..f61d1cd649b 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -52,13 +52,10 @@ 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"; + /// 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/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 64c46d76fb2..0e6b109b0ce 100644 --- a/kernel/src/arch/x86_64/boot.rs +++ b/kernel/src/arch/x86_64/boot.rs @@ -141,7 +141,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 f733e9e9f98..bdc25b5b815 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; @@ -46,7 +45,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/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/kernel/src/device.rs b/kernel/src/device.rs index bf369954bcc..0000fe6a09b 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); } @@ -181,20 +179,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..3e977b701d2 100644 --- a/kernel/src/mouse.rs +++ b/kernel/src/mouse.rs @@ -1,27 +1,22 @@ +//! 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 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}; 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(); @@ -29,121 +24,41 @@ pub static WATCH: Watch = Watch::new(); #[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), -]; +static IN_USE: Sources = Sources::new(); 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); - 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) } - /// 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; `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), + // 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, scroll, kind, x, y }); } /// Throw away everything queued, for the reason [`crate::keyboard::discard_queued`] gives. @@ -151,34 +66,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. - IN_USE[source.0 as usize / 64].fetch_and(!(1u64 << (source.0 % 64)), Ordering::Relaxed); - published +/// 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.give_back(source.0); } 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/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/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..43df3d91171 --- /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::{Merge, Pointer, Slot, State}; diff --git a/lib/input/src/merge.rs b/lib/input/src/merge.rs new file mode 100644 index 00000000000..c8b85df34e8 --- /dev/null +++ b/lib/input/src/merge.rs @@ -0,0 +1,187 @@ +//! 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. +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 } + } +} + +#[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/src/wire.rs b/lib/input/src/wire.rs new file mode 100644 index 00000000000..cea288cacb0 --- /dev/null +++ b/lib/input/src/wire.rs @@ -0,0 +1,156 @@ +//! 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: 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. +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. +pub 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..809ae04ef53 --- /dev/null +++ b/lib/input/tests/input.rs @@ -0,0 +1,161 @@ +//! 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::{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() { + // 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 * 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)); + 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)); +} diff --git a/lib/manifest/src/lib.rs b/lib/manifest/src/lib.rs index 62d9eeb662f..d31a73973a9 100644 --- a/lib/manifest/src/lib.rs +++ b/lib/manifest/src/lib.rs @@ -160,6 +160,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}; @@ -276,6 +285,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 { @@ -560,6 +581,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 8c9edf6a224..db548978ea2 100644 --- a/system/compositor/desktop/src/lib.rs +++ b/system/compositor/desktop/src/lib.rs @@ -39,7 +39,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 c709f1a1996..b56e1873fd7 100644 --- a/system/compositor/src/main.rs +++ b/system/compositor/src/main.rs @@ -44,18 +44,27 @@ 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, the prompt's two and the clipboard's two, -/// and [`Poller::MAX_HANDLES`] is the widest set one poller can carry. Unlike the memory budget this does not move when the +/// ones, the pending connections, the prompt's two, the clipboard'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 - CLIPBOARD_POLL_HANDLES; + Poller::MAX_HANDLES + - FIXED_POLL_HANDLES + - MAX_PENDING_CONNS + - PROMPT_POLL_HANDLES + - CLIPBOARD_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 c2f69aef256..e0e2c6657c5 100644 --- a/system/compositor/src/session.rs +++ b/system/compositor/src/session.rs @@ -15,13 +15,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::clipboard::{Landing, Watch}; use toyos_desktop::{ cursor_from_abs, cursor_style, fold_mouse, hit_test, key_action, set_mode, tab_action, Chrome, @@ -39,9 +42,18 @@ use crate::render::{self, Assets, BackBuffer, SystemStats, TitleBarIcons}; use crate::stats::FrameTotals; use crate::{ launcher_apps, CLIPBOARD_POLL_HANDLES, 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" @@ -116,6 +128,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 @@ -182,6 +200,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"); @@ -250,12 +270,18 @@ 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 and the - // clipboard's two beside them, and `MSG_SET_RESOLUTION` can raise - // `max_windows` mid-run. + // live window and one per pending connection, the prompt's two, the + // clipboard'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 + CLIPBOARD_POLL_HANDLES, + FIXED_POLL_HANDLES + + MAX_WINDOW_SLOTS + + MAX_PENDING_CONNS + + PROMPT_POLL_HANDLES + + CLIPBOARD_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); @@ -285,6 +311,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, @@ -358,6 +388,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 clipboard_ready = self.is_ready(self.clipboards.as_handle()); @@ -379,7 +413,15 @@ impl Session { } self.pending.retain(|p| now.duration_since(p.since) < HANDSHAKE_TIMEOUT); - if !kb_ready && !mouse_ready && !accept_ready && !prompt_ready && !clipboard_ready && !client_ready { + if !kb_ready + && !mouse_ready + && !inputs_ready + && sources_ready.is_empty() + && !accept_ready + && !prompt_ready + && !clipboard_ready + && !client_ready + { return false; } @@ -390,6 +432,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(Port::Compositor); } @@ -404,7 +452,7 @@ impl Session { self.watched(); self.reap(); self.prompt_gone(); - self.rearm(kb_ready, mouse_ready, accept_ready, prompt_ready, clipboard_ready); + self.rearm(kb_ready, mouse_ready, inputs_ready, accept_ready, prompt_ready, clipboard_ready); true } @@ -416,88 +464,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(); - } - } - KeyAction::SpawnTerminal => self.launch("/system/bin/terminal"), - KeyAction::Paste => { - if let Some(i) = focused { - self.paste(i); - } + 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; } - // 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(); + if let Some(i) = focused { + deliver(&mut self.dead, &self.stack[i], window::MSG_KEY_INPUT, &event); } - 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; } @@ -707,6 +785,70 @@ 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)); + } + 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 `port`. fn accept(&mut self, port: Port) { // `accept` installs a descriptor, so it answers `ResourceExhausted` on @@ -1330,6 +1472,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. @@ -1382,7 +1525,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, clipboards: bool) { + fn rearm(&mut self, kb: bool, mouse: bool, inputs: bool, acceptor: bool, prompts: bool, clipboards: 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..48df57ba406 --- /dev/null +++ b/system/ps2server/src/bus.rs @@ -0,0 +1,212 @@ +//! 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(); + } + } +} + +/// 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/fake.rs b/system/ps2server/src/fake.rs new file mode 100644 index 00000000000..a2c39e765f4 --- /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` 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. + 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: None, + 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(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..65aebd9510c --- /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, Records}; + +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 } + } +} + +impl Records for Hw { + fn clock(&mut self) -> u64 { + toyos_abi::clock::nanos_since_boot() + } + + /// The claim's record, if it holds one. + fn take(&mut 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:?}"), + } + } + + 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 { + 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 { + self.clock() + } + + fn wait(&mut self, deadline: u64) -> Option { + self.first(deadline) + } +} diff --git a/system/ps2server/src/main.rs b/system/ps2server/src/main.rs new file mode 100644 index 00000000000..a9645702d46 --- /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..d7849fbc6bf --- /dev/null +++ b/system/ps2server/src/probe.rs @@ -0,0 +1,532 @@ +//! 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:** 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`, +//! 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. **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. +//! +//! 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 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; +/// The aux stage's: TP 9's 500 ms of diagnostics, and its acknowledgements. +const AUX_NS: u64 = 600_000_000; + +/// 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). +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, 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 { 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 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 { 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.said_drained(&format!("waiting to write {what}")); + (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, 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 => 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 3: 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 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; + 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, Vec, Vec) { + match probed(fake) { + (Probed::Armed { aux, backlog }, said) => (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 (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"); + 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 (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 (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"); + 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 (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); + 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 (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))); + } + + #[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 (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 (_, _, said) = armed(&mut fake); + 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 (_, _, said) = armed(&mut fake); + 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); + } + + /// 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.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 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(); + 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..713441eb307 --- /dev/null +++ b/system/ps2server/src/stream.rs @@ -0,0 +1,461 @@ +//! 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, 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}; +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. + Aux::Off | Aux::Owed { .. } => { + self.counts.discarded += 1; + return; + } + } + 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. + out.push(pointer(buttons, dx as i16, dy as i16)); + } + MouseOutcome::Reset => self.aux = Aux::Owed { failures: 0 }, + MouseOutcome::Discarded => self.counts.discarded += 1, + } + 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(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; + } + 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" + )); + } +} + +/// 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; + 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:#?}"); + } + + /// 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(); + 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, [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:#?}"); + } + + /// 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 e08aab8c67e..7cb9d7ef77d 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -560,7 +560,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| { @@ -1074,7 +1074,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..6b2b9278809 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,14 @@ 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) } + +/// `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/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 index 77546a24f04..3113fc81abc 100644 --- a/tests/guest/rust-tests/src/bin/isa_claim_refused.rs +++ b/tests/guest/rust-tests/src/bin/isa_claim_refused.rs @@ -1,5 +1,8 @@ -//! 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. +//! 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; @@ -14,10 +17,9 @@ fn main() { 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", + "a claim on a row whose port the kernel declared answered {:?}, not PermissionDenied", other.map(|_| ()) ), } - println!("the claim on a controller the kernel drives was refused PermissionDenied"); + println!("the claim on a row with a port the kernel declared 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..84cb19310f7 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 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. 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,48 @@ 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, 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); + poller.watch(&claim, READABLE, 0); + 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/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/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] diff --git a/tests/images/metalcase.toml b/tests/images/metalcase.toml index 8589fa8c5b6..cddef6dfb16 100644 --- a/tests/images/metalcase.toml +++ b/tests/images/metalcase.toml @@ -9,22 +9,24 @@ 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 # 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 -serves = ["compositor", "prompt", "clipboard"] +serves = ["compositor", "prompt", "clipboard", "input"] receives = ["soundserver"] devices = ["framebuffer", "keyboard", "mouse"] @@ -45,9 +47,21 @@ devices = ["pci:1af4:1041"] service = true receives = ["netstack"] -# The union its guest binaries need on this machine shape. +# 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; `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/images/panelcase.toml b/tests/images/panelcase.toml index 9d2e5bc4144..6db530a40be 100644 --- a/tests/images/panelcase.toml +++ b/tests/images/panelcase.toml @@ -16,5 +16,5 @@ syscap = ["logread"] [programs.compositor] service = true -serves = ["compositor", "prompt", "clipboard"] +serves = ["compositor", "prompt", "clipboard", "input"] devices = ["framebuffer", "keyboard", "mouse"] diff --git a/tests/metal/src/lib.rs b/tests/metal/src/lib.rs index 01ff007df19..e74f495f923 100644 --- a/tests/metal/src/lib.rs +++ b/tests/metal/src/lib.rs @@ -752,11 +752,13 @@ 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", + // 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. @@ -807,12 +809,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 bb7953417c5..bc9287762d7 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,11 +108,15 @@ 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", "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. @@ -156,14 +160,16 @@ 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. "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. @@ -806,7 +812,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. @@ -814,6 +820,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()) }, + ), ( // Every CPU's microcode line, held to what its bench is held to: // `microcode_on_metal` says which machine is held to what. @@ -823,7 +837,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, @@ -893,10 +907,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 { @@ -959,14 +969,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()) @@ -978,13 +988,30 @@ 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()) }, }, ), + ( + // 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 { @@ -997,12 +1024,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"], )]; @@ -1064,7 +1092,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, @@ -1075,7 +1104,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 @@ -1104,7 +1138,6 @@ const SELFTEST_PARAMS: &[&str] = &[ "xhci-xecp-selftest", "xhci-descriptor-selftest", "sysret-ss-probe", - "test-input-merge", "sched-operation-nesting", "iommu-firmware-left", ]; @@ -1831,8 +1864,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")], @@ -1850,7 +1883,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 @@ -1999,7 +2031,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> { @@ -2011,7 +2044,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 @@ -4072,14 +4108,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. ///