diff --git a/Cargo.lock b/Cargo.lock index 38806704f28..6ebbb9bd155 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1962,13 +1962,10 @@ dependencies = [ "toyos-acpi", "toyos-amdvi", "toyos-blackbox", - "toyos-blockhold", "toyos-bootmap", "toyos-dma", "toyos-elf", - "toyos-fat32", "toyos-gicv3", - "toyos-gpt", "toyos-its", "toyos-microcode", "toyos-pci", @@ -1976,15 +1973,12 @@ dependencies = [ "toyos-quiesce", "toyos-random", "toyos-rootimage", - "toyos-scsi", "toyos-smmu", "toyos-symbols", "toyos-tco", "toyos-tsc", - "toyos-usbhid", "toyos-userbound", "toyos-wallclock", - "toyos-xhci", ] [[package]] @@ -2386,6 +2380,7 @@ dependencies = [ "toyos-blockring", "toyos-blockserve", "toyos-gpt", + "toyos-quiesce", ] [[package]] @@ -4446,7 +4441,6 @@ dependencies = [ "toyos-userbound", "toyos-vdagent", "toyos-wallclock", - "toyos-xhci", "ureq", "webpki-root-certs", ] @@ -4596,6 +4590,7 @@ name = "toyos-inspect" version = "0.1.0" dependencies = [ "toyos-abi", + "toyos-gpt", ] [[package]] @@ -4894,6 +4889,7 @@ dependencies = [ name = "toyos-usbhid" version = "0.1.0" dependencies = [ + "toyos-abi", "toyos-input", ] @@ -4960,15 +4956,13 @@ dependencies = [ name = "toyos-xhci" version = "0.1.0" dependencies = [ + "toyos-abi", + "toyos-blockring", + "toyos-device-memory", + "toyos-gpt", + "toyos-input", "toyos-scsi", -] - -[[package]] -name = "toyos-xhci-sim" -version = "0.1.0" -dependencies = [ - "rand 0.9.5", - "toyos-xhci", + "toyos-usbhid", ] [[package]] @@ -5142,7 +5136,12 @@ version = "0.0.0" dependencies = [ "toyos", "toyos-abi", + "toyos-blockring", + "toyos-blockserve", + "toyos-input", "toyos-inspect", + "toyos-pci-claim", + "toyos-quiesce", "toyos-xhci", ] diff --git a/Cargo.toml b/Cargo.toml index 4d1094a0b94..ba693aaadc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,6 @@ members = [ "kernel/phys", "kernel/sim", "kernel/smmu", - "kernel/usbhid", "lib/acpi", "lib/acpiserver-api", "lib/bcachefs", @@ -73,13 +72,12 @@ members = [ "lib/tmpdir", "lib/trace", "lib/tsc", + "lib/usbhid", "lib/update", "lib/userbound", "lib/vdagent", "lib/virtio", "lib/wallclock", - "lib/xhci", - "lib/xhci/sim", "loader", "sdk/abi", "sdk/filepicker", @@ -140,6 +138,7 @@ members = [ "system/update", "system/update/copies", "system/usbd", + "system/usbd/xhci", "system/vdagent", "system/vdagent/bridge", "tests/guest/kernelprobe", @@ -296,9 +295,6 @@ toyos-userbound = { path = "lib/userbound" } # The scheduler's quantum and the windows' hold, so the harness judges a boot # against the constants the kernel schedules by. kernel = { path = "kernel" } -# The Bulk-Only phases, so the harness judging a wedge reads the word -# `toyos_xhci::bot::Phase` declares instead of spelling it a second time. -toyos-xhci = { path = "lib/xhci" } # The ACPI server's count line, so the judge reads the words the server writes # and the job that waits for them matches. acpiserver-api = { path = "lib/acpiserver-api" } diff --git a/build/bootlog.rs b/build/bootlog.rs index 679cefcc716..00a1e5d14d5 100644 --- a/build/bootlog.rs +++ b/build/bootlog.rs @@ -67,18 +67,6 @@ pub const SEAL_PC: &str = " pc="; /// that names the wrong instruction. pub const WEDGE_SPIN: &str = "kernel::deadline::this_cpu+"; -/// What the `usb-reset-under-load` arm says once it is streaming, and the three -/// ways it says it is not, in `kernel/src/usb_gate.rs`. -/// -/// **The witness that the reset landed on a busy bus.** A boot whose page -/// carries the first line and none of the other three is one whose reset found -/// a controller still moving bytes; any of the other three is the idle case -/// under this arm's name. -pub const USB_LOAD_RUNNING: &str = "usb-load: sweeping disk 0"; -pub const USB_LOAD_REFUSED: &str = "usb-load: refused"; -pub const USB_LOAD_STOPPED: &str = "usb-load: the disk stopped answering"; -pub const USB_LOAD_SWEPT: &str = "usb-load: the sweep reached the end of the disk"; - /// What one CPU's own NMI writes into the black box when that CPU has taken no /// interrupt for its bound, in `kernel/src/hardlockup/mod.rs`. /// @@ -703,10 +691,6 @@ mod tests { "kernel/src/deadline.rs", format!("fn {}() -> !", WEDGE_SPIN.trim_end_matches('+').rsplit("::").next().expect("a path")), ), - ("kernel/src/usb_gate.rs", format!("LOAD_RUNNING: &str = \"{USB_LOAD_RUNNING}\"")), - ("kernel/src/usb_gate.rs", format!("LOAD_REFUSED: &str = \"{USB_LOAD_REFUSED}\"")), - ("kernel/src/usb_gate.rs", format!("LOAD_STOPPED: &str = \"{USB_LOAD_STOPPED}\"")), - ("kernel/src/usb_gate.rs", format!("LOAD_SWEPT: &str = \"{USB_LOAD_SWEPT}\"")), ("kernel/src/hardlockup/mod.rs", format!("LOCKED_UP: &str = \"{LOCKED_UP}\"")), ("kernel/src/hardlockup/mod.rs", format!("\"{LOCKUP_PC}{{}}\", At(pc)")), ("kernel/src/sync.rs", "pub fn lock(&self) -> LockGuard<'_, T>".to_string()), diff --git a/build/build.rs b/build/build.rs index 1d2202bc9ce..cfc73bf78de 100644 --- a/build/build.rs +++ b/build/build.rs @@ -615,22 +615,28 @@ const SUPERVISOR_PROGRAM: &str = "supervisor"; /// row receives it. const SUPERVISOR_SERVED: &[&str] = &[toyos_swap::PORT, "power", toyos_manifest::grants::PORT]; -/// 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, the -/// input port, whose holder types into the focused window, the clipboard -/// port, whose holder is sent every copy made in the session, and the display -/// port, whose holder sets the mode and the scale every window is drawn at. -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), - (toyos_manifest::display::PORT, toyos_manifest::display::HOLDER), +/// The ports that named rows alone may receive, each with those rows: 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, the input port, whose holders type into the focused window, the +/// clipboard port, whose holder is sent every copy made in the session, and +/// the display port, whose holder sets the mode and the scale every window is +/// drawn at. +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::HOLDERS), + (toyos_manifest::clipboard::PORT, &[toyos_manifest::clipboard::HOLDER]), + (toyos_manifest::display::PORT, &[toyos_manifest::display::HOLDER]), ]; +/// Rows, as a refusal names them. +fn named(rows: &[&str]) -> String { + rows.iter().map(|r| format!("`{r}`")).collect::>().join(" and ") +} + /// Who may hold the authorities that change what the machine runs or what a /// package sees: each of [`HELD_ALONE`]'s ports, its holder and nothing else /// — no other `[programs]` row and never `[apps]` — the idle slot, @@ -645,9 +651,9 @@ const HELD_ALONE: &[(&str, &str)] = &[ fn held_by_their_holders_alone(config: &SystemConfig) -> Result<(), String> { use toyos_manifest::consent; for (name, program) in &config.programs { - for (port, holder) in HELD_ALONE { - if name != holder && program.receives.iter().any(|r| r == port) { - return Err(format!("`{name}` receives `{port}`, which only `{holder}` may hold")); + for (port, holders) in HELD_ALONE { + if !holders.contains(&name.as_str()) && program.receives.iter().any(|r| r == port) { + return Err(format!("`{name}` receives `{port}`, which only {} may hold", named(holders))); } } if program.receives.iter().any(|r| r == consent::PORT) { @@ -665,9 +671,9 @@ fn held_by_their_holders_alone(config: &SystemConfig) -> Result<(), String> { return Err(format!("`{name}` asks for `slots` and is in `[boot] start`, and the slots are granted to a launch alone")); } } - for (port, holder) in HELD_ALONE { + for (port, holders) in HELD_ALONE { if config.apps.receives.iter().any(|r| r == port) { - return Err(format!("`[apps] receives` names `{port}`, which only `{holder}` may hold")); + return Err(format!("`[apps] receives` names `{port}`, which only {} may hold", named(holders))); } } if config.apps.receives.iter().any(|r| r == consent::PORT) { @@ -3225,14 +3231,15 @@ mod tests { } } - /// The input port reaches ps2server alone, and never `[apps]`: a - /// connector types into the focused window. + /// The input port reaches ps2server and usbd alone, and never `[apps]`: + /// a connector types into the focused window. #[test] - fn only_ps2server_holds_the_input_port() { + fn only_the_keyboard_drivers_hold_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(())); + assert_eq!(gate("[programs.usbd]\nreceives = [\"input\", \"disk\"]\n[programs.compositor]\nserves = [\"input\"]\n"), Ok(())); for (text, said) in [ - ("[programs.paint]\nreceives = [\"input\"]\n", "`paint` receives `input`, which only `ps2server` may hold"), + ("[programs.paint]\nreceives = [\"input\"]\n", "`paint` receives `input`, which only `ps2server` and `usbd` may hold"), ("[apps]\nreceives = [\"input\"]\n", "`[apps] receives` names `input`"), ] { match gate(text) { diff --git a/build/ci.rs b/build/ci.rs index 76892775320..c782b5296c0 100644 --- a/build/ci.rs +++ b/build/ci.rs @@ -497,9 +497,6 @@ pub(crate) const CONTROLS: &[Control] = &[ red(KERNEL_LOOM, "lock-acquire-off", Some("ticket_lock"), &[ Fails("try_lock_observes_the_previous_owners_writes"), ]), - red(KERNEL_LOOM, "owed-fence-off", Some("owed_lock"), &[ - Fails("a_turned_away_cpu_holds_the_lock_or_is_answered"), - ]), red(KERNEL_LOOM, "seqlock-writer-fence-off", Some("panic_console_publish"), &[ Fails("a_snapshot_is_one_publication_whole"), ]), diff --git a/build/clippy.rs b/build/clippy.rs index 409571a1153..a7fdffc3a96 100644 --- a/build/clippy.rs +++ b/build/clippy.rs @@ -59,9 +59,7 @@ fn control_features() -> Vec { /// set never sees; the third is the one `--kernel-param` builds, `boot-actuators` /// without `test-actuators`, whose dead code neither of the others can see. /// `undocumented_unsafe_blocks` is adopted per area as each area's -/// justifications land. `toyos-xhci` has a shape of its own because the -/// workspace run builds it only with `toyos-xhci-sim`'s `flaws`, never as the -/// kernel does. `toyos-pci-claim` has one because `$GUESTS` excludes it and +/// justifications land. `toyos-pci-claim` has one because `$GUESTS` excludes it and /// only netstack's host tests build its `host-grant`. `kernel-loom` has one /// because `victim-retires-mid-probe`'s test arm excludes `no-preempt-guard`, /// which `$CONTROLS` turns on beside it. `toyos-crypto` has three because the @@ -150,10 +148,6 @@ const SHAPES: &[Shape] = &[ before: &["-p", "bootloader", "--target", Arch::Aarch64.loader()], after: &["$ADOPTED", "-W", "clippy::undocumented_unsafe_blocks", "-D", "warnings"], }, - Shape { - before: &["-p", "toyos-xhci", "--all-targets"], - after: &["$ADOPTED", "-D", "warnings"], - }, Shape { before: &["-p", CRYPTO, "--all-targets", "--features", CRYPTO_DISPATCHED], after: &["$ADOPTED", "-D", "warnings"], diff --git a/build/hostws.rs b/build/hostws.rs index 4b8fba024cd..2fbad63e4d3 100644 --- a/build/hostws.rs +++ b/build/hostws.rs @@ -311,7 +311,7 @@ mod tests { #[test] fn the_gate_refuses_a_crate_that_joined_neither_list() { let members: BTreeSet = - [".", "lib/elf", "lib/xhci", "lib/xhci/sim"].iter().map(|s| s.to_string()).collect(); + [".", "lib/elf", "system/usbd", "system/usbd/xhci"].iter().map(|s| s.to_string()).collect(); let all_declared: BTreeSet = members.clone(); assert!(unclaimed(&members, &all_declared).is_empty()); @@ -324,8 +324,8 @@ mod tests { // And a nested one. let with_a_nested_newcomer: BTreeSet = - members.union(&["lib/xhci/newthing".to_string()].into()).cloned().collect(); - assert_eq!(unclaimed(&members, &with_a_nested_newcomer), ["lib/xhci/newthing"]); + members.union(&["system/usbd/newthing".to_string()].into()).cloned().collect(); + assert_eq!(unclaimed(&members, &with_a_nested_newcomer), ["system/usbd/newthing"]); } /// The walk has teeth only if it can find anything: it must reach the real @@ -338,7 +338,7 @@ mod tests { assert!(found.contains("."), "the walk did not find the root package"); assert!(found.contains("lib/elf"), "the walk did not find lib/elf: {found:?}"); assert!( - found.contains("lib/xhci/sim"), + found.contains("system/usbd/xhci"), "the walk did not descend past the first level, so a nested member could \ go missing without this gate noticing: {found:?}" ); diff --git a/build/image.rs b/build/image.rs index d485386c037..9b4eab87db7 100644 --- a/build/image.rs +++ b/build/image.rs @@ -376,6 +376,29 @@ pub fn read_file_on(file: &mut std::fs::File, guid: [u8; 16], name: &str) -> Res Ok(bytes) } +/// Every file in the root of the image's log volume (its one Microsoft basic +/// data partition), by name, read with the driver the kernel mounts it with. +pub fn log_volume_files(file: &mut std::fs::File) -> Result)>, String> { + let part = only_partition(&mut FileSectors(file), toyos_gpt::Guid::MICROSOFT_BASIC)?; + let (start, len) = (part.first_lba() * u64::from(LBA), part.lba_count().get() * u64::from(LBA)); + let len = usize::try_from(len).map_err(|_| format!("a {len}-byte volume"))?; + let mut volume = vec![0u8; len]; + file.seek(SeekFrom::Start(start)) + .and_then(|_| file.read_exact(&mut volume)) + .map_err(|e| format!("reading the log volume at byte {start}: {e}"))?; + let mut fs = Fat32::mount(VolumeIo(&mut volume)).map_err(|e| format!("the log volume does not mount: {e}"))?; + let entries = fs.read_dir("/", usize::MAX).map_err(|e| format!("listing the log volume: {e}"))?; + let mut files = Vec::new(); + for entry in entries.into_iter().filter(|e| !e.is_dir) { + let path = format!("/{}", entry.name); + let mut found = fs.open(&path).map_err(|e| format!("opening {path}: {e}"))?; + let mut bytes = vec![0u8; usize::try_from(found.len()).unwrap_or(usize::MAX)]; + fs.read(&mut found, 0, &mut bytes).map_err(|e| format!("reading {path}: {e}"))?; + files.push((entry.name, bytes)); + } + Ok(files) +} + /// Why a scan's `out[0]` and `matched` count did not pick out exactly one /// partition, once the table itself was readable. pub enum OnePartitionError { diff --git a/build/sourcegate.rs b/build/sourcegate.rs index f29ea82ac48..b5e2d64a26b 100644 --- a/build/sourcegate.rs +++ b/build/sourcegate.rs @@ -100,7 +100,6 @@ const NMI_SILENT: &[&str] = const BUS_MASTER_SITES: &[(&str, usize)] = &[ ("kernel/src/drivers/pci.rs", 1), ("kernel/src/drivers/virtio.rs", 1), - ("kernel/src/drivers/xhci/wait/boot.rs", 1), ("kernel/src/arch/x86_64/amdvi/mod.rs", 1), ]; @@ -307,10 +306,7 @@ const HOSTNAME: &str = "a hostname carrying a personal name"; /// Every value of a refused shape the tree keeps, by file, each identifying /// nobody. A value a shape passes says the same without a row here: a locally /// administered MAC, an RFC 5737 or RFC 3849 address. -const IDENTIFIES_NOBODY: &[(&str, &str)] = &[ - // Made up, in the kernel's own record of a disk that came back. - ("tests/checks/usb.rs", "FEDCBA98765432FEDCBA"), -]; +const IDENTIFIES_NOBODY: &[(&str, &str)] = &[]; /// Every maximal run of `line`'s bytes that `of` takes, and where it starts. /// `of` takes ASCII alone, so a run begins and ends on a character. diff --git a/images/console.toml b/images/console.toml index b839a9198f1..0421b94421b 100644 --- a/images/console.toml +++ b/images/console.toml @@ -49,7 +49,7 @@ provides = ["surface"] serves = ["input"] receives = ["log"] starts = ["shell"] -devices = ["framebuffer", "keyboard", "mouse"] +devices = ["framebuffer"] [programs.shell] receives = ["surface"] @@ -107,6 +107,13 @@ serves = ["block", "disk"] drives = ["pci:01:08:02/1b36:0010"] receives = ["disk"] +# The USB driver: one process per xHCI controller, its sticks offered to the +# block service as disks it serves, its keyboards and pointers reported to the +# owner of input. +[programs.usbd] +drives = ["pci:0c:03:30"] +receives = ["disk", "input"] + # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when # one ends, on the same ports. diff --git a/images/system.toml b/images/system.toml index 3f9d3eae96a..3184d0f5ebd 100644 --- a/images/system.toml +++ b/images/system.toml @@ -84,7 +84,7 @@ receives = ["power"] service = true serves = ["compositor", "prompt", "clipboard", "input", "display"] receives = ["soundserver", "filepicker", "volume"] -devices = ["pci:03:80:00/1af4:1050", "framebuffer", "keyboard", "mouse"] +devices = ["pci:03:80:00/1af4:1050", "framebuffer"] starts = ["terminal", "files", "/apps"] login = true @@ -276,6 +276,13 @@ serves = ["block", "disk"] drives = ["pci:01:08:02/1b36:0010"] receives = ["disk"] +# The USB driver: one process per xHCI controller, its sticks offered to the +# block service as disks it serves, its keyboards and pointers reported to the +# owner of input. +[programs.usbd] +drives = ["pci:0c:03:30"] +receives = ["disk", "input"] + # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when # one ends, on the same ports. diff --git a/issues/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md b/issues/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md index c6087899a6f..a16752a5849 100644 --- a/issues/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md +++ b/issues/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md @@ -120,7 +120,7 @@ is owed; a cause is. ## What is known and what is not -- `issues/an-xhci-storm-starves-the-cpu-that-takes-it.md` carries the +- The kernel's xHCI storm, closed when that driver left the kernel (the small-kernel track's step 10), carried the same shape, over the 34 s the stick was being written, on run 20's `metaldevicecase` boot at tip `f46f91eb` (`[2026-09-07 08:51:28 34.609 cpu1] irq: cpu0 total=103171 timer=10 xhci=103161 net=0 sound=0 i8042=0 dmafault=0 hda=0 tlb=0 nmi=0 spurious=0 unclaimed=0`) diff --git a/issues/a-bind-whose-device-left-aims-the-ladder-at-its-empty-port.md b/issues/a-bind-whose-device-left-aims-the-ladder-at-its-empty-port.md deleted file mode 100644 index 319eab309b1..00000000000 --- a/issues/a-bind-whose-device-left-aims-the-ladder-at-its-empty-port.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-30 ---- - -# A bind whose device left aims the ladder at its empty port - -Derived from the code, not staged. - -`kernel/src/drivers/xhci/wait/msc.rs` has two exits for a device that left its -port. `scsi()` answers a round trip that broke as `Broke::Gone` itself: it -sends no recovery and leaves the disk to its port's teardown. `bring_up` does -not. Its TEST UNIT READY hands every break to `climb_until_in_step`, a `Gone` -one included, with `Left::Elsewhere`. The climb enters at the class reset, whose -steps are aimed at a port that reads empty. Only once that rung has ended does -the climb read the port and find the device gone. - -The class rung's bound is spent on a device that is not on the bus, and a -device that left during its bind is handed to the teardown by another path -than one that left during a command. `main` at 3c24b6edb has the same shape: -its bind climbs a `Gone` break too, and the port rung's reset is where it -first reads the port empty. - -**Exit**: the port is read once, at the climb's entry as well as after each -rung, so a break whose device left ends there before any rung. `scsi()`'s -`Broke::Gone` arm then goes, and a device that left is one exit. diff --git a/issues/a-claimed-function-can-storm-its-cpu.md b/issues/a-claimed-function-can-storm-its-cpu.md index ce6e329bd36..0a56149e1d1 100644 --- a/issues/a-claimed-function-can-storm-its-cpu.md +++ b/issues/a-claimed-function-can-storm-its-cpu.md @@ -14,9 +14,10 @@ the ITS (`kernel/src/arch/aarch64/irqchip/its.rs`) — and `pcidev::isr` holder programs the device through the BARs it maps, so a hostile or broken holder can make the device send messages as fast as the bus carries them, and each one is an interrupt cpu0 takes before anything else it runs. -`issues/an-xhci-storm-starves-the-cpu-that-takes-it.md` measured what -that does to a CPU for a kernel driver's source; a claim is the same source -with its holder outside the kernel. +The kernel's own xHCI measured what that does to a CPU for a kernel +driver's source, ten timer interrupts in thirty-four seconds on T14 run 20, +before that driver left the kernel; a claim is the same source with its +holder outside the kernel, which every xHCI controller now is. The lever is the claim's own translation: on x86-64 its remapping entry, which made not present stops the function's messages at the unit; on AArch64 its diff --git a/issues/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md b/issues/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md index 485df2b4622..ad56c35f248 100644 --- a/issues/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md +++ b/issues/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md @@ -7,9 +7,12 @@ opened: 2026-09-25 # A console's watch waits on the keyboard, and its readiness is the serial line A blocking read of a console was moved onto the serial line's own wait; a -watch through an inbox (`Poller::watch` of a console handle) was not, so -serial input never completes it: a program that polls its console for input -on a machine whose console is a serial port is not woken by a key typed there. +watch through an inbox (`Poller::watch` of a console handle) was not. Its one +source was the kernel's keyboard watch, which went with the kernel's input +streams (the small-kernel track's step 10), so a console has no readable +watch at all now: registering one is refused, and libc's `poll` reads every +completed token as ready, a refusal included (`sdk/libc/src/posix_io.rs`), so a +program that polls its console for input spins rather than waits. ## Exit condition diff --git a/issues/a-cut-bulk-only-command-does-not-brick-the-benchs-stick.md b/issues/a-cut-bulk-only-command-does-not-brick-the-benchs-stick.md index d0c55603291..3f10f2e99ab 100644 --- a/issues/a-cut-bulk-only-command-does-not-brick-the-benchs-stick.md +++ b/issues/a-cut-bulk-only-command-does-not-brick-the-benchs-stick.md @@ -92,18 +92,28 @@ a device that does not honour it is one no software on a laptop without VBUS control can clear. This bench's device honours it in every state the table above records. A different device, or this one in a state no wedge reaches, may not. -## The present state, in the run's own words - -Run 47 is the standing guard doing its job for the first time: one boot per -suite writes to the bench's own stick continuously, is reset out from under -itself, and is judged on whether the device comes back. `power::usb_load_chain` -PASSed on it, `boot.usbload.stick_secs` read 0 against a ceiling of 5, and the -whole account was on the page — which is the other thing that boot established, -because no wedge page this bench had taken before carried one. +## The present state + +**Every survival in the table above was measured with the kernel's port +reset in place**: until #971 every reset this kernel performed reset every +connected port, halted and reset the controller and unpowered its ports +before the reset register. That path and its guard, `power::usb_load_chain` +on `usb-reset-under-load`, went with the kernel's xHCI. + +Since #971 the orderly stop does the same from userland: the supervisor tells +every disk driver the machine stops, after the file servers' sync, and usbd +resets every connected port and its controller before the kernel's stop +(`toyos_xhci::hc::Hc::stop`). The boot deadline, the hard-lockup detector and +a panic reset with no USB register touched, as #901 §6 accepted, and so does +a stop whose usbd died, its function reset by its claim's release. The +guard for that path is `stick_survives_a_reset_under_load`: `stick_load` +writes the stick through usbd until the boot deadline resets the machine, +judged on `boot.usbload.stick_secs` against the bench's record. It has not +run yet. ## Exit condition A reproduction of the hang itself — not of a reset — that leaves evidence off the stick, and an account of what the controller and the device were in when it -happened. `boot.usbload.stick_secs` stands as the regression guard meanwhile, and it reds -if a reset ever does brick the device. +happened. The regression guard is `stick_survives_a_reset_under_load`'s +`boot.usbload.stick_secs`. diff --git a/issues/a-device-function-is-driven-from-its-arrival-to-its-end.md b/issues/a-device-function-is-driven-from-its-arrival-to-its-end.md index 58928562b34..015a924b8e5 100644 --- a/issues/a-device-function-is-driven-from-its-arrival-to-its-end.md +++ b/issues/a-device-function-is-driven-from-its-arrival-to-its-end.md @@ -146,7 +146,11 @@ started. Nothing reaches userland when a device arrives or leaves. `issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`, whose `input` port is this track's per-device source and `gone` (`toyos-input` carries both), and whose sticks are disks offered on `disk` (the ruling - above). + above). **Done**, its metal rows that judge with a userland load + still owed at that step. The test images name each machine's boot + controller by id, and the AMD bench's by place (`@03:00.3`, the one + `drives` selector a place narrows), so the spares stay free for the claim + tests. ## Folded here diff --git a/issues/a-device-swapped-into-its-port-inside-the-port-rungs-reset-is-taken-for-the-disk-it-replaced.md b/issues/a-device-swapped-into-its-port-inside-the-port-rungs-reset-is-taken-for-the-disk-it-replaced.md deleted file mode 100644 index 0bd196910b5..00000000000 --- a/issues/a-device-swapped-into-its-port-inside-the-port-rungs-reset-is-taken-for-the-disk-it-replaced.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-28 ---- - -# A device swapped into its port inside the port rung's reset is taken for the disk it replaced - -Derived from the code, not staged. - -The port rung (`port_reset_recovery` in `kernel/src/drivers/xhci/wait/msc.rs`) -resets the port and reads it once (`reset_port`). When -`toyos_xhci::ladder::after_reset` answers `Enumerate`, the port reads -connected, enabled and at the speed the disk was bound at. The rung then -addresses the disk's own slot again, sets its old configuration, adds its old -bulk pair and asks TEST UNIT READY. **Nothing re-reads who the device is.** - -Take a device pulled after the reset completes and another plugged into the -same port before that read. On a USB3 port the new device trains to Enabled by -itself (§4.19.1.2). The CSC its arrival raised is the one -`enumeration_ack(Some(Warm), …)` spends, because a warm reset's retrain raises -one too (§4.19.5.1) and nothing tells the two apart. A second unit of the same -model answers every step: every identity field is the same but its serial -number, as with `usb_transport_break`'s `AnotherStick`. The rung then carries -disk 0's volume on onto it. - -The port machine does not see the swap either. Its belief stays attached with -disk 0's slot, and the rung's acknowledge spent the edge. - -`usb_transport_break` moves the stick to port 3, so the bind's serial check -(`XhciController::adopt`) judges it there. Nothing stages the same port. - -**Exit**: a disk the port rung took back is the device it was. Its serial -number is read again and judged by `toyos_xhci::identity::same` before the -volume carries on. A `usb-reset-moves` staging that plugs another serial number -into the same port refuses it by name. - -`usb_transport_break` is deleted; `issues/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md` records the commit that restores it. diff --git a/issues/a-disk-plugged-in-after-boot-is-bound-inside-a-scheduling-pass.md b/issues/a-disk-plugged-in-after-boot-is-bound-inside-a-scheduling-pass.md deleted file mode 100644 index c5c18c48b97..00000000000 --- a/issues/a-disk-plugged-in-after-boot-is-bound-inside-a-scheduling-pass.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -status: open -kind: track -opened: 2026-08-10 ---- - -# A disk plugged in after boot is bound inside a scheduling pass - -The BOT round trip and the SCSI bring-up above it are pure machines, -`toyos_xhci::bot::RoundTrip` and `toyos_scsi::BringUp`, and the kernel -drives both blocking, in place. For the read/write entry points that is the -caller's own time. For the bind it is not: `msc::bind` is the one call site -where a scheduling pass can still spend its transfer budget inside xHCI — for a -disk arriving *after* boot, which is one greppable path. - -**What to build**: a stepped driver for the bind over the same two machines, -one act per pass, as enumeration has. Moving the bind to a thread that may -block — usbd, step 10 of -`issues/the-kernel-is-small-interrupts-post-and-threads-wait.md` — ends -this file as well. - -After it, the pass-duration proof costs no new code: one guest gate measuring a -scheduling pass across a plug, plus the existing check-build's pass-cost -distribution. Both premises are now spent. - -Constraints the machine has to preserve: - -- **One outstanding command, not a queue.** The command ring is one queue and - the driver is strictly serial; a completion event must be matched against the - command TRB's *physical address*, or a timed-out command hands its code to - whoever waits next. -- **A control transfer with a data stage is two completions** — the data stage - carries ISP and IOC, and the status stage is a second event on the same - (slot, dci) — so a submission takes a stage count and the answer carries the - residue as well as the code. -- The lock disables preemption for the guard's whole life, warns at 50M spins - and panics at 500M, against a 2 s per-command budget. **The ticket lock is - excluded as the T14 freeze's mechanism** — the owner saw neither the warning - nor the panic. Do not re-litigate that. -- Rust makes a module's private items visible to its **descendants**, so a - *view* handed to the poll enforces nothing. The split had to be a module, with - the poll outside it. -- **There is no gate for the 100 ms unplug window and it cannot be aimed** — a - QEMU `device_del` cannot land inside it. That is the answer, not an omission. - Relatedly, QEMU's xHC has no link training and no inactive state: its - SuperSpeed ports read enabled the moment they are touched, so warm-reset - correctness lives in the host model only. - -`issues/pulling-the-boot-stick-freezes-the-t14.md` is open and is not -closed by this. diff --git a/issues/a-disk-plugged-in-after-settling-is-routed-as-soon-as-it-is-offered.md b/issues/a-disk-plugged-in-after-settling-is-routed-as-soon-as-it-is-offered.md new file mode 100644 index 00000000000..92fb5711d2b --- /dev/null +++ b/issues/a-disk-plugged-in-after-settling-is-routed-as-soon-as-it-is-offered.md @@ -0,0 +1,23 @@ +--- +status: open +kind: defect +opened: 2026-10-21 +--- + +# A disk plugged in after settling is routed as soon as it is offered + +diskserver routes nothing until every disk driver it was started with has +settled (`Known::open`, `system/diskserver/src/main.rs`). After that, a disk +that arrives later is routed from its own offer alone. A stick plugged into a +running machine is one; usbd is already settled when the stick arrives. + +A partition GUID is routed to the first disk that offers it. If a second disk +later offers the same GUID, `offer` refuses it on both disks for any later +open, but every session already routed to the first disk stays open. A client +that opens before the later disk's offer arrives is refused as if no disk +carried the GUID. It is not told that a disk is still arriving. + +**Exit**: diskserver's host tests show the rule. Either sessions already open +on a GUID that a later offer duplicates are ended, or the open stays routed +and the duplicate is refused, with that choice stated at `offer`. Owner: +`system/diskserver`, held by the orchestrator until USB hot-plug is assigned. diff --git a/issues/a-disk-refused-while-one-is-held-is-enumerated-again-and-no-test-reaches-that-arm.md b/issues/a-disk-refused-while-one-is-held-is-enumerated-again-and-no-test-reaches-that-arm.md deleted file mode 100644 index a115d929cf7..00000000000 --- a/issues/a-disk-refused-while-one-is-held-is-enumerated-again-and-no-test-reaches-that-arm.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# A disk refused while one is held is enumerated again, and no test reaches that arm - -`refuse_for_now` (`kernel/src/drivers/xhci/device.rs`) gives a disk refused as -not ready, or for want of a pool block, a Disable Slot with -`AfterSlot::Again` while a disk on this controller is held for its device. The -arm in `slot_gone` (`kernel/src/drivers/xhci/mod.rs`) frees the refused -device's block, tears its port down so it is enumerated again, and logs -`xHCI: port N is enumerated again while a disk is held for its device`. - -Nothing reads that line: `rg 'enumerated again while a disk' tests/` finds -nothing, and no run has shown whether any guest test reaches the arm. The -decision is taken in the kernel and not in `toyos-xhci`, so no host test can -stage it either. - -**Owner**: the xHCI driver, `kernel/src/drivers/xhci/`. - -**Exit**: a test stages a disk refused while another is held, and asserts that -its port is enumerated again and the disk bound. With `AfterSlot::Again` -replaced by `AfterSlot::Refused`, that test goes red. diff --git a/issues/a-disk-taken-offline-is-never-brought-back.md b/issues/a-disk-taken-offline-is-never-brought-back.md index 9852ee041a9..45b14bdf3f0 100644 --- a/issues/a-disk-taken-offline-is-never-brought-back.md +++ b/issues/a-disk-taken-offline-is-never-brought-back.md @@ -6,32 +6,27 @@ opened: 2026-09-16 # A disk taken offline is never brought back -A mass-storage device whose transport breaks climbs `toyos_xhci::ladder`: the -class reset, then a port reset after which the device is addressed and -configured again on the slot, the pool block and the disk number it already -has, so a mount on it carries on. A device that does not answer after the port -reset is taken offline behind one more reset, its slot goes back, and every -later operation on it is refused until it is unplugged. On the T14 that disk is -the root filesystem, so the boot is over from there. - -Two ways a disk that could still be served is lost today: - -- **The port reset moves it.** T14 runs 74 and 79: a SuperSpeed stick +A mass-storage device whose transport breaks climbs usbd's ladder +(`system/usbd/xhci/src/msc.rs`, over `toyos_xhci::ladder`): the class +reset, then a port reset and enumeration again, its disk held meanwhile for +its return. A device that does not answer after the port reset is taken +offline: the last thing it is sent is a reset, its disk ends `Gone` with every +session on it, and its port keeps it, unasked, until it is unplugged. On the +T14 that disk carries the log volume and the data volume. + +- **The port reset may move it.** T14 runs 74 and 79: a SuperSpeed stick enumerated on the USB2 half of its receptacle answered the bus reset by training SuperSpeed on the other half, so it left port 1 and arrived on port - 13. `toyos_xhci::identity` now holds such a disk for `RETURN_WINDOW` and - gives its number back to a device that proves the same identity. + 13. usbd holds the disk for `identity::RETURN_WINDOW` after its own reset + and gives it back, sessions and queued work whole, to a device that proves + the same identity on any port + (`a_stick_back_from_its_port_reset_keeps_its_disk_and_its_work`, on the + stub). No bench has measured the move under usbd. - **Offline is for the connection's life.** Nothing asks an offline device again, however long it stays plugged in. -Both come to the same missing piece: a mount holds a disk number for life -(`usb_storage::handle`, `release_blocks`), and nothing re-attaches a number to -a device that is the same device. - ## Exit condition A T14 boot whose stick's transport broke mid-boot and that finishes with its -mounts intact, on either half of the receptacle; and a disk that left its port -under a reset and arrived on another keeps its number. - -`usb_transport_break` is deleted; `issues/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md` records the commit that restores it. +mounts intact, on either half of the receptacle; and an offline device asked +again, or the reason it must not be, stated. diff --git a/issues/a-disk-whose-port-went-away-panics-the-boot-at-roots-hold.md b/issues/a-disk-whose-port-went-away-panics-the-boot-at-roots-hold.md deleted file mode 100644 index eff424d7928..00000000000 --- a/issues/a-disk-whose-port-went-away-panics-the-boot-at-roots-hold.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# A disk whose port went away panics the boot at ROOT's hold - -`usb_transport_break` fails its boot with: - - PANIC: panicked at src/rootfs.rs:177:19: - boot: the partition ROOT was read from, , cannot be held: Unusable - -`rootfs::hold_source` (#506, 265a0fce) runs after the USB gate. It asks -`gpt::claimable` for ROOT's partition, and that reads every registered disk's -table. The test arms `usb-port-gone`, which ends the gate's last read on its -disk as a port that read disconnected and leaves the disk to its port's -teardown. That disk's table read then fails, `table_refused` answers -`Unusable`, and `hold_source` panics, although ROOT is on the other stick. - -Measured at 9583d913 merged with 5e446e5c: -- `cargo test --test toyos-build -- --nightly usb_transport_break` EXIT=1, - red alone as well. -- The same run with `usb-port-gone` removed from the test's `PARAMS`, as a - checked patch that was restored: the boot no longer panics, and the test - fails only on the port-gone assertion it no longer staged. - -The nightlies agree: -- Red the same way on main's nightly at e8d7c9c0 (run 36228604597). -- Red at fd62f567 (run 36278449733, guest (7)). -- On the nightly at 3f46a019 (run 36111884575, before #506) it failed for a - different reason. - -**Exit**: a disk that left during boot is not a disk ROOT's hold must read, -or the boot's refusal is what `usb_transport_break` expects. Either way -`usb_transport_break` is green on a nightly. - -`usb_transport_break` is deleted; `issues/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md` records the commit that restores it. diff --git a/issues/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md b/issues/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md deleted file mode 100644 index 992a920c4ea..00000000000 --- a/issues/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-22 ---- - -# A held disk waits for a pass no CPU takes when every CPU is in a call on it - -A disk whose device left under this driver's reset is held for the device to -come back, and a call on it only waits for the verdict: the returning device is -enumerated and bound by the port machine on a CPU that reaches a scheduler pass -(`kernel/src/drivers/xhci/wait/msc.rs`, `wait_for_return`). A disk call spins -with `IF` clear, so when every CPU is inside a call on the held disk, no CPU -takes that pass until the calls end on their bounds, and each answers -`BudgetExpired`. - -**Measured under QEMU, `smp:2`**: `usb_transport_break`'s moved-stick boot at -`99a81a8c` (`utb-rateb2-usbtransport-9.log` in the job scratchpad): cpu0's -write and cpu1's read of disk 0 both held from 0.356 s; port 3 read connected -throughout and was first enumerated at 2.455 s, after both calls ended at -2.354 s. At that head the window also ran out first and disk 0 was lost; since -`f0695038` a held disk is not forgotten while a port reads connected and -untaken, so the disk is taken back and the retried operations complete (1 of 6 -runs at `f0695038` took this shape and passed). - -**A CPU spinning on a lock the call's caller holds counts too.** -`usb_transport_break --nightly` at `e889d03e` (#554), the `AnotherStick` boot -(`554r6-usb_transport_break.log` in the job scratchpad), red. cpu0 spent 0.383 -to 4.385 s in logd's create of the boot's log file, which holds `vfs::lock()` -(`object::ops::open`): two writes of block 9351 on held disk 0, each ending -`still held` on its bound, at 2.384 and 4.385, with no pass between them. -cpu1 logged nothing from 0.311 to 4.390 s. Two threads resumed within a -millisecond of that create's end: `test-runner`'s spawn of `reboot`, whose own -`total=7ms` puts its start at about 4.383, and init's `started test-runner` -line, stamped 4.386 for a spawn made at 0.363. An idle cpu1 kicked by -`wait_for_return` would have torn port 1 down within its 100 ms debounce, and -no `port 1 disconnected` line exists, so cpu1 took no pass. That it spun on the -VFS lock is inferred, not measured. Port 3 read connected and untaken -throughout, so the arrival rule kept disk 0 held and no `did not come back` -line came either. The other stick was never enumerated, and the test's -`is not disk 0 come back` line never came. From 4.424 s cpu0 spun in the -reboot's sync and cpu1 in a shootdown -(`a-shutdown-on-a-held-usb-disk-left-a-cpu-deaf-to-a-tlb-shootdown.md`). - -**What is still wrong**: the operation that waited answers `BudgetExpired` -instead of the device's answer, and a caller that gives up on one — `logd` on a -refused create (`issues/logkeeper-ends-the-boots-log-on-one-refused-create-and-nothing-durable-says-so.md`) -— loses what it was doing. The retry loops above the block layer spin with `IF` -clear too, so on a machine with one CPU the device binds only when the caller -leaves the kernel. - -**A demand fill spins across calls.** `file_backing::read_block_retrying` -retries `BudgetExpired` for up to `block::DEADMAN` (120 s) without leaving the -kernel, since a fill cannot park. A fill on a held disk therefore spins call -after call: each is inside `CALL_AFTER_BREAK`, and their sum is bounded only by -the deadman. Derived from the code in review round 2 of #466, not staged. - -**Not staged**: the `ARRIVED` mutation (the arrival rule removed) survives a -run of `usb_transport_break`, because nothing makes every CPU enter a call on -the held disk; the interleaving comes at a rate. - -**Exit**: a disk call that waits for a device does not hold a CPU with `IF` -clear — the wait parks — or a held call's CPU may bind within its bound without -making the bind the caller's cost. Either is the owner's ruling to revisit. - -Related records — `usb_transport_break`'s three other open red modes, not this -one: `issues/a-shutdown-on-a-held-usb-disk-left-a-cpu-deaf-to-a-tlb-shootdown.md`, -`issues/usb-transport-break-flushedstick-can-break-after-the-reboot.md`, and -`issues/a-disk-whose-port-went-away-panics-the-boot-at-roots-hold.md`. - -**Unrun since it was disabled**: PR #562 deleted this test's timing check, -that the staged break's record came less than 2 s of kernel clock after the -record before it. That change has never run: the test's first run back is -also that change's. - -**`4f2bea143` holds #588's version of the test**: `git show 4f2bea143:tests/common/usb.rs`. -`usb_stick_left`'s T14 row arms `usb-transport-break`, so it stays; `usb-reset-moves`, -`usb-reset-moves-after` and `usb-reset-moves-configured` went with its QEMU arm in #660. diff --git a/issues/a-hid-interface-outside-the-boot-subclass-is-read-as-qemus-tablet.md b/issues/a-hid-interface-outside-the-boot-subclass-is-read-as-qemus-tablet.md deleted file mode 100644 index 6da6c22f3d3..00000000000 --- a/issues/a-hid-interface-outside-the-boot-subclass-is-read-as-qemus-tablet.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-09 ---- - -# A HID interface outside the boot subclass is read as QEMU's tablet - -`kernel/src/drivers/xhci/device.rs`'s `parse_config` binds every HID -interface of subclass 0, whatever its protocol, as `HidType::Tablet`, and -`toyos-usbhid`'s `Pointer::Tablet` reads its reports in the layout of QEMU's -`usb-tablet`: buttons, X and Y little endian, the wheel. No report descriptor -is read, so that is true of QEMU's tablet and of nothing else by construction. -A keyboard's second interface carrying its media keys, a mouse left in report -protocol, a security key or a UPS each publishes a subclass-0 interface; its -reports are refused by length or, at six bytes, land as absolute pointer -positions. Nothing in reach has been seen doing it: the T14's own USB devices -bind no HID interface at all (`0 HID device(s)` in a `testcases` readback). - -Owned by usbd, the small-kernel track's step 10 -(`issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`), which -binds every HID interface from userland. - -**Exit condition.** A subclass-0 interface is bound only from its report -descriptor, read by a parser in `toyos-usbhid` that refuses one it cannot -read by name, and a host test feeds it QEMU's tablet descriptor and one of -another layout. diff --git a/issues/a-machine-that-boots-off-nvme-looks-1500-ms-for-a-usb-boot-disk.md b/issues/a-machine-that-boots-off-nvme-looks-1500-ms-for-a-usb-boot-disk.md deleted file mode 100644 index ba8b2790220..00000000000 --- a/issues/a-machine-that-boots-off-nvme-looks-1500-ms-for-a-usb-boot-disk.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-09 ---- - -# A machine that boots off NVMe looks 1500 ms for a USB boot disk - -`gpt::probe_usb_disks` (`kernel/src/gpt.rs`) keeps asking the USB disks for -the partition firmware booted from until one carries it or -`xhci::PORT_SETTLE_CEILING`, 1500 ms, has passed. On a machine whose image is -on its NVMe disk no USB disk ever carries it, so every boot spends the whole -ceiling, on the boot CPU, before a task runs: paced by `PORT_POLL` in a -`spin_loop`, with nothing else scheduled. - -Measured on `nvme_disk_keeps_log_and_home`'s machine, which has no USB -controller at all, on both of its boots: the kernel says `xHCI: no controller -on this machine, USB input unavailable` and, stamped 1501 ms later (`2.917` -and `4.418` on the test's first run), `usb-storage: 0 disk(s) on this machine -and none carries the boot partition after 1500 ms of looking`. Those boots -report `kernel to Boot: complete 1741 ms`. And on `bar_map_again`'s, a -`Headless` machine whose xHCI carries a keyboard and no disk: `usb-storage: 0 -device(s)` at `6.126` and the same line at `7.628`. - -The kernel already knows the outcome it waits for is impossible on a machine -with no xHCI controller, and it is told nothing about which bus the boot -partition's disk is on: the loader has the device path and hands over the -partition's GUID and extent alone. - -**Exit condition.** A machine that boots off a disk this kernel does not -drive spends none of that ceiling: the line above is gone from -`nvme_disk_keeps_log_and_home`'s boots, or the wait is deleted with the -kernel's xHCI at step 10 of -`issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`. - -## Owner - -`kernel/src/gpt.rs`; unheld. diff --git a/issues/a-machine-without-an-iommu-refuses-every-claim.md b/issues/a-machine-without-an-iommu-refuses-every-claim.md index e9547d3225c..b2ea7e87b5d 100644 --- a/issues/a-machine-without-an-iommu-refuses-every-claim.md +++ b/issues/a-machine-without-an-iommu-refuses-every-claim.md @@ -37,10 +37,17 @@ pci:1b36:0010 is on this machine and the kernel refused this service its claim`), so the three file servers say `Data serving … — absent`, `Log serving /log — absent` and `Boot serving /boot — absent`, and `logkeeper` says `no /log on this machine - this boot's kernel log is on the console only`. Read off the no-unit arm of `iommu_virtio_platform`, which is -green with it: the arm asserts the refusal and reads no volume. Where the -image is on a USB stick the kernel drives, LOG and BOOT are served through -kernel partition claims, which need no unit, and DATA alone is absent; that -is read off the claim path and not run. +green with it: the arm asserts the refusal and reads no volume. **The kernel +drives no USB either** since usbd took the xHCI (the small-kernel track's step +10), so on such a machine usbd is refused its controller too, a USB stick's +volumes are absent like the NVMe disk's, and no USB keyboard or pointer +reaches the owner of input; the arm asserts that refusal as well. A machine +whose unit remaps no message, or filters no SMI, refuses the same claims for +those reasons (QEMU's AMD unit is both, by profile). On a bench that is the +stick too, so such a boot leaves no `/log` for a metal judge to read: the +metal rows that staged it (`iommu-no-remap`) are deleted, and QEMU's +`usbd_refused_without_amdvi_remapping` and `amdvi_firmware_left` hold the +refusal. Exit, in both arms of `iommu_virtio_platform` (`tests/common/iommu.rs`): diff --git a/issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md b/issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md index 832568b9c9d..ae29d3d7c07 100644 --- a/issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md +++ b/issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md @@ -23,6 +23,10 @@ and whether real hardware shows it, where the stick is not an emulated one. Nothing in `logd` is special about the write: any program writing that much to the stick while a tone plays is the same stimulus. +Since the small-kernel track's step 10 the stick is usbd's: the interrupts are +its claim's and the waits its thread's, and the kernel's two-second transport +break is gone. Whether the starvation remains is unmeasured. + ## Exit condition On the T14, a `METAL` row plays `hda_tone`'s tone while a program writes a diff --git a/issues/a-metal-row-names-no-hardware-and-a-bench-without-it-reds.md b/issues/a-metal-row-names-no-hardware-and-a-bench-without-it-reds.md index 392be5c1a0f..6b755632e3c 100644 --- a/issues/a-metal-row-names-no-hardware-and-a-bench-without-it-reds.md +++ b/issues/a-metal-row-names-no-hardware-and-a-bench-without-it-reds.md @@ -16,8 +16,9 @@ that most rows ride: - `test_rs_pci_reclaim` exited 1, `claim 1 of 8086:15fc was refused: NotFound`: the job claims the T14's I219, which that bench has not. The - `claim_reuses_its_remapping_entry` and `claim_refused_without_remapping` - rows read that job. + `claim_reuses_its_remapping_entry` row reads that job (and so did + `claim_refused_without_remapping`, deleted by #971: without remapping a + bench drives no USB, so its boot left no `/log`). That run asked for `blackbox_unclaimed_page` alone, which passed. An unfiltered `cargo test --test toyos-build -- --metal --metal-host diff --git a/issues/a-poll-that-leaves-a-port-outstanding-with-no-wake-says-nothing.md b/issues/a-poll-that-leaves-a-port-outstanding-with-no-wake-says-nothing.md deleted file mode 100644 index b375d54c663..00000000000 --- a/issues/a-poll-that-leaves-a-port-outstanding-with-no-wake-says-nothing.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-28 ---- - -# A poll that leaves a port outstanding with no wake says nothing - -The simulator's pump (`lib/xhci/sim/src/driver.rs`) fails with -`Stuck::Unwoken` when a pass leaves the port outstanding -(`PortState::outstanding`) with no instant to come back at. The kernel has no -counterpart. `XhciController::poll` (`kernel/src/drivers/xhci/mod.rs`) returns -`None` in that state, `poll_if_pending` stores 0 in `PORT_WORK_AT`, and the port -is not read again until some other xHCI interrupt arrives. On hardware nothing -names the state, and the simulator catches it only on the arms it models. - -**Owner**: the xHCI driver, `kernel/src/drivers/xhci/`. - -**Exit**: `poll` logs a line naming the port whenever it returns `None` with a -port outstanding. diff --git a/issues/a-process-lengthens-an-interrupts-off-walk-by-the-threads-it-parks-on-one-ring.md b/issues/a-process-lengthens-an-interrupts-off-walk-by-the-threads-it-parks-on-one-ring.md index 05a50b7054e..c6022441310 100644 --- a/issues/a-process-lengthens-an-interrupts-off-walk-by-the-threads-it-parks-on-one-ring.md +++ b/issues/a-process-lengthens-an-interrupts-off-walk-by-the-threads-it-parks-on-one-ring.md @@ -57,7 +57,7 @@ mutated kernels) read the same report of the same load at 1980239 - 2 × 5075290 is one of eight: every CPU reads 2 × 5014552 to 2 × 5075290 (`:416` to `:430`), beside `tlb: shootdowns=282 wait=131633us max=10038us` (`:431`). It is a log write's all-CPU shape - (`issues/xhci-waits-are-spins.md`) by reading. + (the kernel's xHCI waits, since gone with that driver) by reading. - 4725822 is cpu0's, a firmware step's length by reading (`issues/the-t14s-firmware-interrupts-every-cpu-every-2-2-s-under-toyos.md`). The other seven read 544283 to 1018355. diff --git a/issues/a-reset-stops-xhci-and-leaves-every-claimed-pci-function-armed.md b/issues/a-reset-stops-xhci-and-leaves-every-claimed-pci-function-armed.md deleted file mode 100644 index 49d0d2dc9bd..00000000000 --- a/issues/a-reset-stops-xhci-and-leaves-every-claimed-pci-function-armed.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-16 ---- - -# A reset stops xHCI and nothing else: a claimed PCI function whose holder never exits is left armed when the register is written - -**The reset paths, as the tree has them.** `kernel/src/drivers/acpi.rs:12-14`: -"[`reset_now`] and [`shutdown`] are the only two places this kernel writes a -register that ends the machine, and each stops every xHCI controller -([`stop::before_reset`]) before it does". Five paths reach those two: - -- `sys_reboot` (`kernel/src/syscall/machine.rs:113`) → `quiesce` (`:121`) - → `acpi::reboot` (`:122`; `acpi.rs:283`) → `reset_now` (`acpi.rs:290`); -- `sys_shutdown` (`machine.rs:103`) → `quiesce` (`:107`) → `acpi::shutdown` - (`:108`; `acpi.rs:325`); -- `deadline::expire` (`kernel/src/deadline.rs:208`) → `acpi::reset_now` - (`:224`), no `quiesce`; -- `hardlockup::locked_up` (`kernel/src/hardlockup/mod.rs:313`) → - `acpi::reset_now` (`:326`), no `quiesce`; -- `panic_reboot::reboot_now` (`kernel/src/panic_reboot.rs:162`) → - `acpi::reboot` (`:167`) → `reset_now`, no `quiesce`. - -`reset_now` calls `stop::before_reset` (`acpi.rs:313`) and then writes the -port (`:319`); `shutdown` calls it at `:331`. That stop is -`kernel/src/drivers/xhci/stop.rs`'s: one device class. `quiesce` -(`machine.rs:47-100`) disarms the watchdog, syncs, flushes the USB disks' -caches, waits for the log to be durable, seals `DONE` and calls -`xhci::seal_shut`; it kills no process and takes no device from one. - -**A PCI function's own teardown exists, and it is reached from one place.** -`pcidev::release` (`kernel/src/pcidev/mod.rs:782`) calls `tear_down` (`:796`), -which disables bus mastering (`:797`), masks the function's MSI-X entry -(`:798`), empties its IOMMU domain (`:799`) and only then resets the function -(`:809`). `release` is private to `pcidev` and its one caller is -`pcidev::Binding`'s `Drop`; a binding is held by a `device::Claim` and dropped -with it — a dying process's handle table. - -**So on every one of the five paths, a function whose holder is alive is left -as its holder last wrote it when the register is written.** No process is -asked to exit on any of them, so no `Claim` drops, `release` is not called, -and the function's bus-master enable, its MSI-X vector and both rings' base -addresses are whatever the driver programmed — the device may be mid-DMA into -the holder's memory at the write. This tree is in that state on every boot -that runs `netd` and then `reboot`: `system.toml` starts `netd` (`:24`) with -`devices = ["pci:1af4:1041"]` (`:70-72`), a virtio-net function it holds for -its life, and nothing between `/system/bin/reboot`'s `SYS_REBOOT` and the port -write asks it to let go. - -**The I219 is the instance this file was found on, and no boot in this tree -reaches it:** the boot that puts `netd` in front of it, `tests/lancase`, is defined on -PR #442's branch `lan-metal` and on no branch that has merged. The bench boots -this was read on were of those branches. The one that motivated it — run 55's, -ended by `deadline::expire` → `reset_now` — is a path `quiesce` never runs on, -and on that boot the function had already been released: the sealed tail -reads `exit: netd pid=5 code=-1 cpu=22ms` and then -`pcidev: PCI 00:1f.6 [8086:15fc] released from slot 0` at 3.479 s -(`issues/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md`). -So the I219 went into that reset torn down — by the `Drop` a dying process -reaches and a live one does not. - -**What the write does to the function is unmeasured.** The T14's register is -`0xCF9`: the kernel prints `ACPI: reset register SystemIO 0xcf9 <- 0x06` on -every boot there (`acpi.rs:271`; run 42's and run 51's kernel logs, at -0.000 s). Whether a full reset through that port clears a function's COMMAND -bus-master bit and MSI enable, or leaves them for the next operating system's -driver to find, is a platform question this tree has not asked, and nothing -here claims the next boot sees an armed device. What would measure it: a -ToyOS boot chained straight after a `sys_reboot` on which `netd` held the -function — the loop setting `BootNext` again — whose PCI enumeration prints -the function's COMMAND word and MSI control word before anything writes them, -beside the same print after a cold boot. - -## What would answer it - -Either `quiesce` tears down every live claim before the register — its stop -(`kernel/src/quiesce.rs`) halts every userland thread at a safe point and -tears down nothing those threads hold — or the reset path -does for each bound function what it already does for xHCI through -`stop::before_reset`: the register half of `tear_down`, bus mastering off and -the vector masked, written with no lock taken so the two bound-driven paths -can call it too, instead of relying on a `Drop` a live process is never asked -to run. diff --git a/issues/a-shutdown-on-a-held-usb-disk-left-a-cpu-deaf-to-a-tlb-shootdown.md b/issues/a-shutdown-on-a-held-usb-disk-left-a-cpu-deaf-to-a-tlb-shootdown.md deleted file mode 100644 index 40f0c4a4f49..00000000000 --- a/issues/a-shutdown-on-a-held-usb-disk-left-a-cpu-deaf-to-a-tlb-shootdown.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-24 ---- - -# A shutdown on a held USB disk left a CPU deaf to a TLB shootdown for five seconds, and the kernel panicked - -`usb_transport_break` in a local `--nightly` run on the logd -branch at `eef19bd1` (parameter line -`root=…,usb-transport-break,usb-reset-moves,blackbox=0x8000000`, two CPUs, -TCG). Alone it was green. - -cpu1 took the `reboot` job's shutdown into `quiesce` at 1.072 s while the -staged break held disk 0 off its port. Every call it made there ran out of its -operation budget: `vfs: ["boot"] would not sync: would block` at 5.073, -`vfs: ["log"] would not sync: would block` at 7.074, then the censuses. cpu0, -meanwhile, ended a thread (`sys_thread_exit` → `thread_exit` → dropping its -`Unmapped` pages → `arch::tlb::shootdown`) and waited for cpu1's -acknowledgement: - - [kernel 8.088 cpu0 tid=1] PANIC: panicked at src/arch/tlb.rs:171:42: - tlb: cpu 1 has not flushed for generation Generation(1) in 5000000000ns — it is not taking interrupts - -So cpu1 took no interrupt for five seconds somewhere between 3.08 and 8.08 s, -inside the shutdown's sync and flush on a disk that was not coming back. The -panic names no process. The one thread on that boot that ends of its own -accord is logd's network thread, on a boot that gives logd no netd; the logd -branch no longer starts that thread there, which would remove this boot's -trigger and not the deaf CPU. - -`issues/xhci-waits-are-spins.md` carries the arithmetic for one disk -operation outrunning `time::DEAF_CPU`; this is a boot -that did outrun it, in `quiesce`, across several operations each inside its -own budget. Whether `quiesce` holds `IF` clear between them is not measured; -both sightings ran while a syscall's body ran with interrupts masked, and the -shutdown syscall's now runs with them open -(`issues/syscall-preemption-is-incidental.md`), which no boot has -read since. - -**Second sighting, with the roles swapped**: `usb_transport_break --nightly` at -`e889d03e` (#554), the `AnotherStick` boot (`554r6-usb_transport_break.log` in -the job scratchpad). cpu0 took the reboot's `Syncing filesystems` at 4.424 s -and spent it in calls on held disk 0, each ending `still held`: at 6.427, 8.428 -and 10.430. cpu1, in its idle loop, dropped an `InboxRef` in -`object::drain_zero_handles` and waited on cpu0: - - [kernel 9.425 cpu1] PANIC: panicked at src/arch/x86_64/tlb.rs:151:42: - tlb: cpu 0 has not flushed for generation Generation(1) in 5000000000ns — it is not taking interrupts - -## Exit condition - -What holds cpu1's interrupts off across that window is named from a boot, and -either the shutdown's calls on a held disk take interrupts between them or the -shootdown on another CPU cannot need them then; a staged `usb-transport-break` -shutdown with a thread ending on the other CPU inside the window stays up. - -`usb_transport_break` is deleted; `issues/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md` records the commit that restores it. diff --git a/issues/a-stick-that-answers-late-is-broken-by-the-two-second-abandon.md b/issues/a-stick-that-answers-late-is-broken-by-the-two-second-abandon.md index 8d9b97b12aa..02778853b0a 100644 --- a/issues/a-stick-that-answers-late-is-broken-by-the-two-second-abandon.md +++ b/issues/a-stick-that-answers-late-is-broken-by-the-two-second-abandon.md @@ -92,7 +92,13 @@ reported. `stop::OpenCommand` already publishes where the round trip stands; what is missing is the driver keeping it across `with_disk` and `dispatch_event` recording a mass-storage completion nobody is waiting on. +Since the small-kernel track's step 10 the stick is usbd's, which gives one +Bulk-Only transfer ten seconds (`msc::COMMAND_NS`, a policy) before it takes +it back and climbs its ladder; the paragraphs above describe the kernel's +driver, which is gone. + ## Exit condition A T14 boot in which a CSW arrives more than two seconds after its CBW and the -command completes on the caller's retry with no `transport broke` record. +command completes with no break on usbd's ladder, or the measurement that says +ten seconds is not enough. diff --git a/issues/a-stop-mid-append-leaves-a-fat-volume-s-leak-for-ever.md b/issues/a-stop-mid-append-leaves-a-fat-volume-s-leak-for-ever.md new file mode 100644 index 00000000000..80c5c49d86f --- /dev/null +++ b/issues/a-stop-mid-append-leaves-a-fat-volume-s-leak-for-ever.md @@ -0,0 +1,28 @@ +--- +status: open +kind: defect +opened: 2026-10-21 +--- + +# A stop mid-append leaves a FAT volume's leak and stale free count for ever + +A reset, panic or power loss inside a `toyos-fat32` call leaves what its +repair module's open windows name. On the T14's first unclean reset mid-append +(#971, `stick_survives_a_reset_under_load`) that was a log part's chain two +clusters past its size and `FSI_Free_Count` two clusters off. No byte the +size names was lost. `toyos-fat32-check` names both states, and the metal +readback accepts exactly those for a boot its deadline ends +(`toyos_fat32_check::unexplained_by_a_stop`). + +Nothing on the mount path takes them back (`lib/fat32/src/repair.rs`, "The +repair lives in memory"). The clusters stay allocated and unreachable, and +the free count stays the hint it was, which `free_bytes` reports as the +volume's. Every unclean stop adds at most what one call claims. + +**Exit**: a writable mount of a volume that was not cleanly stopped takes +back what a stop can leave: lost chains freed, a chain too long cut to its +size, the FAT copies reconciled, and the free count recounted. It knows the +volume was not cleanly stopped from fatgen103's ClnShutBitMask (FAT[1]), +cleared while writes are open and set again at sync. Shown by the fileserver +crash test mounting each stopped state writable and the checker then finding +nothing. Owner: `lib/fat32`, held by the orchestrator. diff --git a/issues/a-usb3-device-attached-but-untrained-reads-as-gone.md b/issues/a-usb3-device-attached-but-untrained-reads-as-gone.md index d39e2b58aa8..ee3cfea638c 100644 --- a/issues/a-usb3-device-attached-but-untrained-reads-as-gone.md +++ b/issues/a-usb3-device-attached-but-untrained-reads-as-gone.md @@ -11,19 +11,15 @@ Derived from the specification and the code, not staged and not seen on the T14. A USB3 root-hub port that detected a device it could not bring to Enabled reports it with CAS set and CCS clear (xHCI 1.2 §5.4.8, PORTSC bit 24), and a warm reset clears it. `toyos_xhci::portsc::Portsc` names no CAS, and nothing -in `toyos-xhci` or `kernel/src/drivers/xhci` warm-resets a port for it. Such a -port reads `!Portsc::holds()`: - -- the port machine (`toyos_xhci::port::PortState::step`) keys on CCS, so it - tears down what the port held and never enumerates the device; -- the recovery ladder (`climb` and `reset_port` in - `kernel/src/drivers/xhci/wait/msc.rs`) ends a rung whose stick is in that - state as the stick leaving, and resets nothing. +in usbd warm-resets a port for it. The port machine +(`toyos_xhci::port::PortState::step`, which usbd's port tasks step) keys on +CCS, so it tears down what the port held and never enumerates the device; a +disk held for its return after the ladder's port reset is given up on when +its window passes. So a disk whose device lands in that state is lost until it is replugged. Linux reads CAS as a connection and warm-resets the port (`xhci_hub_report_usb3_link_state` in `drivers/usb/host/xhci-hub.c`). **Exit**: `Portsc` names CAS; the port machine warm-resets a USB3 port that -reads it and enumerates what trains; the ladder does not take such a device -for one that left. +reads it and enumerates what trains. diff --git a/issues/an-acknowledge-after-an-enumeration-clears-a-replug-no-look-has-seen.md b/issues/an-acknowledge-after-an-enumeration-clears-a-replug-no-look-has-seen.md deleted file mode 100644 index 0dcbbb125bf..00000000000 --- a/issues/an-acknowledge-after-an-enumeration-clears-a-replug-no-look-has-seen.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# An acknowledge after an enumeration clears a replug no look has seen - -`device::finish`, `device::refuse` and `device::refuse_for_now` -(`kernel/src/drivers/xhci/device.rs`) report the port with -`PortState::enumerated` and then call `acknowledge_port_read`, which writes -back every change flag the read found, CSC included. `wait::boot::scan_ports` -does the same for every port once the boot scan is over -(`acknowledge_port_changes`). - -A replug that lands after the port's last look and before that acknowledge is -cleared before any step reads it. One way in: its Port Status Change Event is -drained by the same `poll` whose `advance_outstanding` ends the enumeration, so -`service_ports` runs after `finish` has written CSC back. The port then reads -connected with no CSC, which is what the driver believes, and the device now in -it is never torn down or enumerated. The look `PortState::believe` owes the -port catches a pull, because CCS reads 0, and not a replug. - -**Not measured.** No test stages it. The simulator's `enumerated` -(`lib/xhci/sim/src/driver.rs`) acknowledges nothing, so no host test can see -it either. - -**Owner**: the xHCI driver, `kernel/src/drivers/xhci/`. - -**Exit**: the simulator's report acknowledges exactly what the kernel's does, -and a sim test that replugs the device between an enumeration's last answer -and the next look expects `ToreDown(Replugged)` and a second enumeration. That -test is red on the kernel's acknowledge and green without it. diff --git a/issues/an-xhci-storm-starves-the-cpu-that-takes-it.md b/issues/an-xhci-storm-starves-the-cpu-that-takes-it.md deleted file mode 100644 index e7c1fc86298..00000000000 --- a/issues/an-xhci-storm-starves-the-cpu-that-takes-it.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-08 ---- - -# An xHCI interrupt storm starves the CPU that takes it out of its own timer - -T14 run 20's own census, over the 34 s the stick was being written — the -`metaldevicecase` boot at tip `f46f91eb`, the `irq:` block at 34.609 s -(`[2026-09-07 08:51:28 34.609 cpu1] irq: cpu0 total=103171 timer=10 xhci=103161 net=0 sound=0 i8042=0 dmafault=0 hda=0 tlb=0 nmi=0 spurious=0 unclaimed=0`): - - irq: cpu0 total=103171 timer=10 xhci=103161 ... - irq: cpu1 total=25 timer=25 xhci=0 - irq: cpu3 total=989 timer=989 - irq: cpu4 total=139 timer=139 - -**cpu0 took ten timer interrupts in thirty-four seconds** and a hundred -thousand xHCI ones. The same span carries - - LOCK CONTENTION: 50M spins at src/vfs.rs:32:18, ticket=38 now=37 - -so another CPU held the VFS lock across a stick write for seconds at a time. - -`arch::idt::xhci`'s handler stamps `irq_ring` and EOIs; the work is -`xhci::poll_if_pending`, which `try_lock`s `XHCI` and **returns doing nothing** -when the lock is held. Nothing masks the source in between, so every event the -controller produces while that lock is held is one more interrupt on the CPU -that cannot service it. The declined CPU halts, owed a kick by the lock's -release, but each new event wakes it into another pass that declines the lock -again, and the cycle costs it the interrupt budget it needed for its own timer. - -That is a CPU making no progress while looking busy, and it is the state -`crate::deadline`'s poll relies on *some* CPU escaping. - -**Where the next reading comes from.** The census above was read out of the -log file, where each process's end had written one. No process's end writes -one now: the machine's census is taken where the machine ends, as records at -its stop and in the record its death seals (`kernel/src/census.rs`). A storm -that ends in the deadline or the lockup detector is read off the black-box -page; one the machine survives leaves no `irq:` line in the file until the -stop, and its rate over a stretch of the boot is not read by anything. - -**What would fix it**: the interrupter's `IMAN.IE` masked when the poll declines -the lock and cleared by whoever takes it — so a controller whose driver is busy -raises one interrupt and not a hundred thousand — or an event-ring drain that -does not need `XHCI` at all. - -**Not this task's**: found while root-causing the loaded-host hang whose cause -was the timer arming and not this. Whether an xHCI storm can by itself hold a -CPU out of its timer for the whole of a bound is unmeasured — and it is one of -the shapes `issues/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md` -leaves open. diff --git a/issues/doom-audio-callback-stalled-on-the-t14.md b/issues/doom-audio-callback-stalled-on-the-t14.md index d06f67ad47e..e3e91febdfe 100644 --- a/issues/doom-audio-callback-stalled-on-the-t14.md +++ b/issues/doom-audio-callback-stalled-on-the-t14.md @@ -23,9 +23,9 @@ device. So the one thing that keeps a 2.9 ms deadline met was absent there and is present in every config the suite runs. That is a mechanism, not a measurement, and two others are equally live: the -`drain_irqs` entry, `issues/scheduler-pass-blocks-in-xhci.md`, -where any syscall on that thread can become the USB -driver's engine for as long as a second; and plain scheduling pressure from a +`drain_irqs` entry, where any syscall on that thread could become the +kernel's USB driver's engine for as long as a second, which this tree no +longer has (the driver is usbd's); and plain scheduling pressure from a game thread and a compositor that never yield to it. What would decide it is the callback's own period count against wall clock, on diff --git a/issues/every-driver-is-still-in-the-kernel.md b/issues/every-driver-is-still-in-the-kernel.md index 34a846c769f..c2bc63a6a7e 100644 --- a/issues/every-driver-is-still-in-the-kernel.md +++ b/issues/every-driver-is-still-in-the-kernel.md @@ -68,9 +68,9 @@ What is left of the staged work: 4. **The i8042 (PS/2)**: staged as stage 7 of `issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`. -- **USB HID cannot move to userspace without moving the boot block device or - splitting the controller.** It shares the controller, the event ring and the - lock with the boot disk. +- **USB HID moved with the boot disk**: usbd drives each xHCI controller + whole, its sticks and its HID devices alike (the small-kernel track's step + 10). - **Where each xHCI keeps its MSI-X table**, which decides whether usbd's claim arms MSI-X or MSI (`bring_up`, `kernel/src/pcidev/mod.rs`). QEMU 11.1's `nec-usb-xhci` arms MSI-X and publishes one memory BAR, BAR 0, so @@ -79,8 +79,8 @@ What is left of the staged work: only BAR 0 and armed MSI in the kernel's own bring-up, with no `MSI-X not armed` line and one remapping entry each (a `testcases` readback): no MSI-X capability came off either walk, so a claim arms MSI and hands BAR 0 over — - if the walk reached the list's terminator. Whether it did is not read: - `drivers/xhci/wait/boot.rs`'s `arm_interrupt` falls back to MSI on + if the walk reached the list's terminator. Whether it did is not read: the + kernel's own xHCI bring-up, which went with step 10, fell back to MSI on `NoCapability::Absent` and `Truncated` alike, and the second refuses the claim as `CapsTruncated`. One T14 boot printing which, or the PCI inventory printing each function's capability ids and how its walk ended, closes it. diff --git a/issues/hotplug-blocks-a-scheduler-pass.md b/issues/hotplug-blocks-a-scheduler-pass.md deleted file mode 100644 index 56dc602b799..00000000000 --- a/issues/hotplug-blocks-a-scheduler-pass.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-08-03 ---- - -# The hotplug enumeration blocks a scheduler pass, and its debounce keeps a CPU awake - -Both are the price of `poll_if_pending` being the only context the driver has, -and both are bounded and paid only by a machine somebody has just plugged into. - -**The enumeration.** `device::configure` runs inline: Enable Slot, Address -Device, three or four control transfers, Configure Endpoint. Under TCG it is -microseconds — the whole hotplug sequence in `xhci_hotplug` is inside one -millisecond of guest time — so nothing in the suite can measure the real cost. -The one hardware figure there is says the T14's five boot-time devices took -346 ms including 5×55 ms of port reset, so roughly **14 ms each** for everything -`configure` does. That is a scheduler pass -of that length on the CPU that services the plug, with preemption disabled under -the `XHCI` lock — the same order as `log_file`'s flush, measured at -2.0–9.7 ms and called out for the same reason; its closing commit `3675af6f7` -carries that measurement. The port reset was the dominant -term and is already out of it; taking the rest out means a state machine over -the control transfers, which is the whole enumeration path rewritten. - -**The debounce.** `PORT_WORK_AT` keeps a CPU with nothing to run out of `hlt` -until the port's deadline, because nothing else would bring it back: the connect -edge was the last interrupt the controller had to give, and the scheduler arms -its one-shot for parked *tasks*. It is a deadline rather than a flag, so the -`XHCI` lock is taken once when it expires and not by every CPU on every pass for -the length of it — but every *idle* CPU declines to halt for the interval, which -is 100 ms for an ordinary plug and up to the 2 s transfer deadline behind a port -that will not reset. Power, never latency: `Action::Idle` is reached only when -there is nothing runnable, and this decides whether to sleep and nothing else. - -What would remove both is a way for a driver to ask the scheduler for a deferred -callback at a deadline — which is also what `i8042::verdict_due` and -`log_ring::file_has_pending` are working around in the same condition. That is a -scheduler-core addition and wants the owner's sign-off. diff --git a/issues/log-flush-retry-deadman-arm.md b/issues/log-flush-retry-deadman-arm.md index f66550e1dee..7be68fdbf59 100644 --- a/issues/log-flush-retry-deadman-arm.md +++ b/issues/log-flush-retry-deadman-arm.md @@ -70,7 +70,7 @@ returned, which is a value nothing on that console prints today. And `log_flush_retry` is re-measured (wide and alone) enough times, on a tree that carries ROOT-in-memory, to say whether either older mode still occurs; if seen again, it is deleted with this file recording the commit that restores -it, or it is fixed at its owner (`kernel/src/drivers/xhci` for the transport line, the boot +it, or it is fixed at its owner (usbd for the transport line, the boot harness for the timeout). If not seen in that many runs, that half closes with the count that supports it. diff --git a/issues/metalprobes-usb-read-is-answered-from-fileservers-cache.md b/issues/metalprobes-usb-read-is-answered-from-fileservers-cache.md index ac79193ba07..0a16697f1d0 100644 --- a/issues/metalprobes-usb-read-is-answered-from-fileservers-cache.md +++ b/issues/metalprobes-usb-read-is-answered-from-fileservers-cache.md @@ -15,5 +15,5 @@ close. So the timed read is fsd's memory, and the `usbread` span is no longer the stick's. **Exit**: the timed read is one the stick answers — a file fsd has not read -since it started, or a read through the partition claim that bypasses the -file server. +since it started, or a read through a block session that bypasses the file +server. diff --git a/issues/partition-claim-departure-exits-clean-with-none-of-its-refusals-said.md b/issues/partition-claim-departure-exits-clean-with-none-of-its-refusals-said.md deleted file mode 100644 index 351734ca156..00000000000 --- a/issues/partition-claim-departure-exits-clean-with-none-of-its-refusals-said.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -status: assigned -kind: tooling -opened: 2026-09-27 ---- - -# `partition_claim_departure` exits 0 having said none of its refusals - -Seen on 2026-09-27 in the nightly run 36336701867, job `guest (5)`, on PR -#541's head `1c0f0c753fb2c4c2d01caf51dc67b7f92ca4cd8e` — a diff that touches -none of `tests/common/partclaim.rs`, `tests/guest/rust-tests/src/bin/partition_claimant.rs` -or the kernel's partition-claim code: - -``` -FAIL partition_claim_departure: departure: the guest exited 0 having said 0 of its 1 refusals: - - FAIL partition_claim_departure (2s) -``` - -Re-run alone, immediately after, in the same session: green, with every role's -own line printed — - -``` - [partclaim] departure: 1 told; [kernel 0.757 cpu0] usb-quiesce: disk 0 SYNCHRONIZE CACHE ok - [partclaim] silent: 1 told; [kernel 0.710 cpu0] usb-quiesce: disk 0 SYNCHRONIZE CACHE ok - [partclaim] untold: 0 told; ... - PASS partition_claim_departure (7s) - ALONE partition_claim_departure: GREEN, and it was alone both times — nothing - the harness controls differed, so it failed once and passed once. That is a - rate and not a classification. -``` - -## What is known - -The failure is `guest_verdict`'s (`tests/common/partclaim.rs`), on the -`departure` role — the first of the three `departed()` iterations in -`partition_claim_departure`. Its message, `"the guest exited 0 having said -{said} of its {refusals} refusals:\n{stdout}"`, prints with an empty tail: the -guest's own captured `stdout` held nothing at all — not one of the `departure` -role's own `println!`s (`"a write is reported and not flushed"`, `"the write -the device left under completed"`, the `refused with` lines `said()` prints, -or `"partition_claimant: PASS"`), yet the guest's exit code was 0. - -An exit of 0 rules out a panic on a wrong assertion (`departure()`'s -`assert_eq!`s all `panic!` on mismatch, and a panic does not exit 0), so the -guest's own logic is not shown to have run into an unexpected state. - -This is the same family flagged in -`issues/partition-claim-gives-up-reds-beside-other-guests-and-is-green-alone.md` -— same test area, and that file already widens its scope to -`partition_claim_departure`. It is filed separately rather than folded in because the failure signature -differs: that file's evidence is a kernel-log line count coming up short -(`"{count} flushes were told of the loss, not {told}"`, matched against -lines the kernel actually printed) on the `silent` role, attributed to a -hypothesised unscoped global fsync deadman race; this is a `guest_verdict` -failure on the `departure` role with the guest's entire stdout capture -missing, which that hypothesis does not by itself explain. - -## Exit condition - -`guest_verdict`'s "exited 0 having said" refusal (`tests/common/partclaim.rs`) -carries the kernel window, as its non-zero-exit refusal already does, so the -next sighting is not blind; the mechanism named and fixed; and a test that -turns red on it deterministically. Then the test is restored and this file is -deleted. - -## Owner - -The partition-claim code, held by the orchestrator. - -**Its test is deleted**: `0e19bf898` took `partition_claim_departure` out, -host and guest halves; `b5c59cbcc` its actuator `usb-transport-break-owed`. -`git show 84471bc58:tests/common/partclaim.rs` holds #536's adaptation of it. diff --git a/issues/partition-claim-gives-up-reds-beside-other-guests-and-is-green-alone.md b/issues/partition-claim-gives-up-reds-beside-other-guests-and-is-green-alone.md deleted file mode 100644 index be95c17546d..00000000000 --- a/issues/partition-claim-gives-up-reds-beside-other-guests-and-is-green-alone.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-25 ---- - -# `partition_claim_gives_up` reds beside other guests and is green alone, and nobody has a rate for it - -Seen on 2026-09-25 in a full fast tier on the dev host (branch `wt/toyos-spawncwd`, -head f9088ea1 plus round-3 review fixes), one run in three: - -``` -FAIL partition_claim_gives_up: deadman: the kernel never said "partclaim: a -write still refused after 1 attempt(s)": - PASS partition_claim_gives_up (6s) - ALONE partition_claim_gives_up: GREEN — it fails only beside other guests, - so its Sched::Parallel is wrong. The run stays red on the classification. -``` - -Re-run immediately after, alone, on the same tree and in the same session: -**green, 2 of 2**, 8-10 s each. So the observation is the same shape as -`usb_short_read`'s and `kernel_log_file`'s: 1 red in 3 with no rate behind it, -under a deadman whose 120 s ceiling and single retry attempt are timed against -wall-clock, not against the guest's own progress. - -Not the diff it was seen from: that branch touches `bcachefs/`, -`kernel/src/bcachefs_adapter.rs`, `kernel/src/main.rs`'s DATA-volume arms, -`build/image.rs`, `tests/common/storage.rs` and a new `home_absent` guest -binary — nothing under `tests/common/partclaim.rs` or the kernel's `partclaim` -module. - -**A rate, from `wt/toyos-inspect` (PR #501, round-2 review fixes), same day, -different diff.** `cargo test --test toyos-build -- partition_claim_gives_up` -alone, 7 back-to-back runs on one dev host: 6 green, 1 red (the deadman case, -same message as above — the boot's own `/log` fsync tripped the actuator's -global deadman first: `log-volume: write ... the device would not answer`, -`fsync: /log/... is not durable after 1 attempt(s)`, before -`test_rs_partition_claimant` ever spawned). 3 runs on the pre-round-2 tree -(`git stash`) were 3 of 3 green. About 1 in 7 to 1 in 10, consistent with "1 in -3" above and with no rate either — both are single sessions. - -Five back-to-back full `cargo test` runs in the same session (nothing else on -the host): 376/376, then `partition_claim_gives_up`'s deadman case alone (as -above), then `partition_claim_departure`'s `silent` case (`0 flushes were told -of the loss, not 1` — its own doc comment names the identical race, "another -claim flushes first — logd's `/log`") plus two unrelated `swap_*` names, then -376/376 again, then `syscall_window_nmi` (this repo's other wall-clock-storm -row, `issues/parallel-tests-red-under-other-suites.md`) plus this test's -*other* case (`unanswered`). No two of the five runs named the same failing -test, and none of the five failures is in a file this round's diff touches -(`kernel/src/block.rs`, `device.rs`, `gpt.rs`, `page_cache.rs`, -`fat32_adapter.rs`, `pcidev/mod.rs`, the xHCI PSIV decode, `toyos-abi`, -`toyos-inspect`, soundd's `inspect.rs`/`mix.rs`) — this round's own dedicated -tests (`inspect_reads_its_owners`, `toyos-inspect`'s and `toyos-abi`'s host -suites, soundd's host tests) stayed green across all five. `uptime` mid-session -read 1-minute 1.92 against 5- and 15-minute 8.30 and 11.26: a host still -carrying the previous runs' own weight, not a second worktree's. Confirms the -race (a global, unscoped `fsync_deadman_now`/`fsync_budget_spent` racing -whichever fsync the boot reaches first) is real and independent of this -session's diff, and widens the exit condition beyond `partition_claim_gives_up` -alone: `partition_claim_departure`'s `silent` case names the same premise in -its own doc comment and should get the same fix. - -**A third rate, from `wt/toyos-ramroot` (PR #506), same shape, same day.** -`cargo test --test toyos-build -- partition_claim` (all three -`partition_claim*` names, `Sched::Parallel`) run alone five times on the -branch head (`390660c4`, which reorders storage to come up after init's spawn -and adds `rootfs::hold_source`) and, alternating, five times on `origin/main` -(`cbc56889`) in a second worktree, same host, same session: - -| run | branch (390660c4) | `origin/main` (cbc56889) | -|-----|--------------------|---------------------------| -| 1 | PASS/PASS/PASS (exit 0) | PASS/PASS/PASS (exit 0) | -| 2 | PASS/PASS/PASS (exit 0) | PASS/PASS/PASS (exit 0) | -| 3 | PASS/PASS/PASS (exit 0) | PASS/PASS/PASS (exit 0) | -| 4 | `partition_claim_gives_up` deadman FAIL, re-run alone GREEN, suite exit 1 | PASS/PASS/PASS (exit 0) | -| 5 | PASS/PASS/PASS (exit 0) | PASS/PASS/PASS (exit 0) | - -Branch: 1 red in 5 (20%). Main: 0 red in 5. The one branch red carries the -identical message and shape already on record above — `FAIL -partition_claim_gives_up: deadman: the kernel never said "partclaim: a write -still refused after 1 attempt(s)"`, immediately green when the harness -re-ran it alone, with the suite's own diagnosis: `ALONE -partition_claim_gives_up: GREEN — it fails only beside other guests, so its -`Sched::Parallel` is wrong`. 1/5 sits inside the 1-in-3-to-1-in-10 band this -file already carries from two unrelated diffs (`bcachefs/` work, an -`inspect`/`toyos-abi` round), and 0/5 on main is unremarkable at that rate — -five trials is too few to call 20% against 0% a rate difference, and the -branch's storage reordering and `rootfs::hold_source` touch none of -`tests/common/partclaim.rs`, the kernel's `partclaim` module, or the fsync -deadman path this file already names as the cause. No code change made on -`wt/toyos-ramroot` for this. - -**Its test is deleted**, as a flaky test is: `fa4c31409` took -`partition_claim_gives_up` out with `partclaim-table-unanswered` and -`partclaim-root-withheld`, the actuators only it armed. It also armed -`fsync-budget-spent` and `fsync-deadman-now`, which `9ebf080e8` took out with -`log_flush_retry`. - -**Exit**: the fsync staging scoped to the claim it is staged for, so the -boot's own `/log` fsync cannot meet it first, and the test restored and green -beside other guests. diff --git a/issues/pulling-the-boot-stick-freezes-the-t14.md b/issues/pulling-the-boot-stick-freezes-the-t14.md index fd3b10003da..52454bd4cd3 100644 --- a/issues/pulling-the-boot-stick-freezes-the-t14.md +++ b/issues/pulling-the-boot-stick-freezes-the-t14.md @@ -88,3 +88,10 @@ class. With `ran=` they read as a time *and* one of two classes, which is what makes a fourth flash worth more than the third was. `usb_boot_stick_pulled` is deleted, so no gate covers the pull. + +## Since usbd + +The kernel holds no USB lock any more: the stick is usbd's (the small-kernel +track's step 10), and a pulled stick is a disk whose sessions end `Gone`, so +the log volume's file server loses its disk and no CPU waits on the device. +Whether the T14 still freezes when its stick is pulled is unmeasured. diff --git a/issues/qemu-drops-console-output-the-harness-is-slow-to-read.md b/issues/qemu-drops-console-output-the-harness-is-slow-to-read.md index b0021e4b007..9981337f335 100644 --- a/issues/qemu-drops-console-output-the-harness-is-slow-to-read.md +++ b/issues/qemu-drops-console-output-the-harness-is-slow-to-read.md @@ -61,6 +61,19 @@ That kernel's ten-second report is gone: the owner ruled on 2026-10-04, choosing entirely": "Delete the periodic report and its counters; hang triage uses the trace diary and panic records instead." A later sighting tells a live kernel from a stopped one by those. +**`crypto_instrument_prints_its_table` hangs this way, and a probe placed every +byte on the guest's side of the pipe.** On `wt/toyos-usbd-2` (host load average 10–15, +other worktrees' guests running) it hung in 9 of 12 runs beside eleven other rows, after +one of its parity jobs printed its table: about 80 KB in one burst. Each hang stopped +mid-line inside that burst. In one boot with temporary probes, logkeeper had read every +line from the runner's ring (its position equal to the ring's head, none refused or +suppressed), and it had written every rendered byte to its console handle, holding none +back. The kernel's console queue was empty and `klogd` owed no record. The runner then +exited by its own watchdog, which proved that it had printed `===TEST_END===` and waited +30 s for a command that never came. The bytes were lost after the guest's virtio +transmit, in QEMU's `virtconsole`. The same hang happened with no `usbd` in the image, and at the branch's base `e07354fb1`, which +has none of its changes: `STALLED: 973s of guard expired` with the same idle guest. + ## Exit condition The console chardev the harness reads cannot refuse a write — for example a diff --git a/issues/redesign-the-log-subsystem.md b/issues/redesign-the-log-subsystem.md index 1111b1ab5b6..e480730e633 100644 --- a/issues/redesign-the-log-subsystem.md +++ b/issues/redesign-the-log-subsystem.md @@ -102,13 +102,10 @@ Cost, so the question is priced: a directory move is `git mv` plus `mod` lines, it touches no logic, and it collides with every worktree in flight — which is why it is a scheduling decision and not a technical one. -Two smaller layout items ride the same answer. `usb_gate.rs` (242 lines) and -`input_merge_test.rs` (178) are correctly `#[cfg]`-gated at declaration and -call (`kernel/src/main.rs:34`, `:36`, `:408`, `:537`) and are never in an -ordinary build, but -they sit interleaved with production sources; the review's target is one -`gates` directory under `kernel/src/` so that what test machinery exists is auditable -in one listing. And `input_merge_test` is the tell that pure logic is trapped -in the kernel: the merge state machine (one held-set, one button-merge, both -bounded) is host-testable with synthetic multi-source streams, after which the -gate shrinks or goes. +Two smaller layout items rode the same answer, and both left the kernel +with its xHCI and input streams (the small-kernel track's step 10): +`usb_gate.rs` and `input_merge_test.rs` sat interleaved with production +sources, and the review's target was one `gates` directory under +`kernel/src/` so that what test machinery exists is auditable in one listing. +`input_merge_test` was the tell that pure logic was trapped in the kernel: the +merge is `toyos-input`'s now, host-tested with synthetic multi-source streams. diff --git a/issues/scheduler-pass-blocks-in-xhci.md b/issues/scheduler-pass-blocks-in-xhci.md deleted file mode 100644 index df8a28cb956..00000000000 --- a/issues/scheduler-pass-blocks-in-xhci.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-08-05 ---- - -# A scheduler pass may spend two seconds in xHCI before it drains its mailbox - -`sched::driver::pass` and `pass_block` both open with `drain_irqs()`, and -`drain_irqs` calls `xhci::poll_if_pending()` — **before** `with_cpu(...)`, and -therefore before the CPU's mailbox drain, its deadline fires and its pick. That -call is not bookkeeping. Its own doc-comment says so: - -> it enumerates hot-plugged devices and recovers broken endpoints, and both spin -> on deadlines measured in seconds while holding `XHCI`, which is a ticket -> spinlock and therefore preemption off for its whole life. - -The deadline is `xhci::USB_TIMEOUT_NS` = 2 s. `cpu::MAX_PASS_NS`, the budget the -scheduler core measures a pass against in `feature = "check"` builds, is 200 µs. -The two numbers disagree by four orders of magnitude, and the driver's prologue -sits on the wrong side of the boundary the budget describes. - -What a CPU inside that recovery holds is *every message addressed to it*: an -`Adopt` carrying a task, a `Wake` for a parked thread, a `Retire`. Nothing in the -scheduler can shorten it — every reap and every wake is bounded by the owning -CPU's pass latency by design, which is exactly why the design is sound. - -``` -retire_task: task not released after 1s: InTransit(CpuId(1)) -``` - -That panic fired on the owner's T14 at 949.792 s of uptime with doom exiting. The -*balance*-path half of it is fixed: `hand_off` reaps a killed task rather than -handing it on, gated by simulator invariant I14. This half is not. - -The second instance of the same shape used to be the idle loop's log flush; -the idle loop touches no filesystem now — the log is logd's file — and the -disk wait that survives, logd's `fsync` pinning a CPU for the device round -trip, is `issues/xhci-waits-are-spins.md`'s subject. - -Closing this means making xHCI enumeration and endpoint recovery asynchronous, so -that `drain_irqs` only ever does work it can finish: drain the event ring, -dispatch HID reports, note that a port or an endpoint owes work. The debounce and -the port reset were already moved off this path for exactly this reason (CLAUDE.md, -USB hotplug); the control transfers inside `configure` and `recover_endpoints` -were not. - -**And the budget cannot see it.** `cpu::MAX_PASS_NS` is measured against by -`SchedPass::finish`, from the `now` the pass was entered with to the end of -`finish_inner()` — and `drain_irqs()` runs *before* `SchedPass::begin`. The -prologue is outside the window the budget covers, so the pass-cost histogram -records a microsecond pass while the CPU had been in the driver for two seconds. -**The measured window has to start where the scheduler entry starts, and that -half of this issue is still open.** diff --git a/issues/screen-fatal-halt-reds-on-ci-with-a-usb-storage-transport-break-during-boot.md b/issues/screen-fatal-halt-reds-on-ci-with-a-usb-storage-transport-break-during-boot.md deleted file mode 100644 index 67486f1cea4..00000000000 --- a/issues/screen-fatal-halt-reds-on-ci-with-a-usb-storage-transport-break-during-boot.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -status: assigned -kind: defect -opened: 2026-09-05 ---- - -# `screen_fatal_halt` reds on CI with a usb-storage transport break during boot, and the runner never says READY - -Merge-queue `ci` run 33996725574 (the queue's composition of #422 on main -5962c745), job 101388609567 `guest (10)`, KVM, one guest per machine: - -``` -FAIL screen_fatal_halt: [qemu] Boot timed out waiting for ===READY===; the console carried: -... -[kernel 0.635 cpu0] spawn: /system/bin/test-runner pid=6 tid=0 ... -init: started test-runner -logd: this boot's kernel log is /log/2026-09-05-230737.log ... -[kernel 2.718 cpu0] usb-storage: 00:02.0 slot 1 transport broke on SCSI 0x35: no answer in the status phase in 2000 ms - FAIL screen_fatal_halt (31s) - ALONE screen_fatal_halt: GREEN, and it was alone both times — nothing the harness controls differed, so it failed once and passed once. That is a rate and not a classification. -``` - -Two facts the log holds. test-runner was spawned at 0.635 s and its first act -is to print `===READY===`, which never reached the console in 31 s. The -SYNCHRONIZE CACHE (0x35) the boot issued to the USB disk got no status-phase -answer in 2000 ms and the transport was declared broken at 2.718 s. Whether the -second stalls the first is the question, and this file is its owner's: the -usb-storage wait path (`kernel/src/drivers/xhci/wait/msc.rs`) and whatever the -runner's stdout blocked on. - -**Exit condition.** Restored when a reproduction pins whether the boot -runner's stdout was blocked behind the usb-storage transport break or the two -are independent, and the wait path stops holding the runner's own output -hostage. Owner: the usb-storage wait path, -`kernel/src/drivers/xhci/wait/msc.rs`; held by the orchestrator. - -**Its test is deleted**: `13ae9aa66` took `screen_fatal_halt` out, and -`git revert 13ae9aa66` brings it back. diff --git a/issues/storage-still-comes-up-before-the-supervisor-runs.md b/issues/storage-still-comes-up-before-the-supervisor-runs.md deleted file mode 100644 index 334d911bc8f..00000000000 --- a/issues/storage-still-comes-up-before-the-supervisor-runs.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-25 ---- - -# Storage still comes up before init runs - -ROOT is the loader's image in memory, and the kernel mounts it and spawns -init with no storage command issued (`root_from_memory` asserts the count -`rootfs::INIT_WITHOUT_A_DISK` logs is zero). But `kernel_main` still brings -up xHCI and reads every USB disk's table (`gpt::probe_usb_disks`) in boot -context after that spawn and before `smp::set_ready` releases the machine, so -both run before init's first instruction. The track's stage 1 exit -(`issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`) -asks for them to be untouched until init runs. - -What holds it there: `xhci::init` binds a USB stick's mass storage — -INQUIRY, READ CAPACITY — inside the one enumeration scan that runs before -there is a scheduler, so a USB-boot machine issues storage commands in it -whatever order the rest takes. The volumes are no longer the kernel's: the -file servers mount them once init starts them. - -Measured on QEMU 11 TCG, USB-stick boot of the `--build-only` image: `Boot: -storage ready` is 118 ms of a 222 ms kernel boot, all of it now after init's -spawn and before init runs. - -Exit: a boot whose storage drivers first speak after init has run, with the -USB bind out of the pre-scheduler scan (the track's stage 5). diff --git a/issues/the-boot-volumes-server-holds-a-claim-that-writes.md b/issues/the-boot-volumes-server-holds-a-claim-that-writes.md deleted file mode 100644 index 794408cc8ab..00000000000 --- a/issues/the-boot-volumes-server-holds-a-claim-that-writes.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-10 ---- - -# The boot volume's server holds a claim that writes - -The boot volume is the ESP firmware loaded the loader from (`Role::Boot`, -`kernel/src/gpt.rs`): on the image the build writes it carries the loader and -`toyos/log.guid`, which the loader reads beside itself; the kernel and its -parameter are on the booted slot's volume (`Role::Slot`), which no file server -serves (`create_esp_volume` and `create_slot_volume`, `build/image.rs`). - -On a disk the kernel drives — the boot stick, until usbd serves it — the -supervisor endows the boot role's file server a partition claim on the ESP -(`storage_endowment`, `system/supervisor/src/main.rs`). A claim carries -`Rights::WRITE` and no `DUP` (`initial_rights`, `kernel/src/object/ops.rs`), so -nothing can hand that server a duplicate narrowed to reading: the loader -firmware runs stays unwritten only by that server's promise to mount the ESP -read-only (`fat_on(disk, writable)`, `system/fileserver/src/main.rs`). On a -disk the block service serves, the same server's grant does not write, and -diskserver answers a write through it `ReadOnly` before the device sees it -(`system/diskserver/src/main.rs`'s module header). - -**Exit**: the boot volume's server on the boot stick holds a partition it -cannot write — a read-only session once usbd serves the stick, or a claim the -kernel mints without `WRITE` — and a test's write through it is refused. - -## Owner - -The usbd cutover of `issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`; unheld. diff --git a/issues/the-committed-binary-gate-skips-a-tracked-path-it-cannot-read.md b/issues/the-committed-binary-gate-skips-a-tracked-path-it-cannot-read.md index 9f1cb1daaa0..22d36ec467a 100644 --- a/issues/the-committed-binary-gate-skips-a-tracked-path-it-cannot-read.md +++ b/issues/the-committed-binary-gate-skips-a-tracked-path-it-cannot-read.md @@ -16,8 +16,7 @@ The one tracked path that does not read today is `rust`, the fork's gitlink, a commit and not a file. `no_tracked_file_identifies_a_machine_or_its_network`, in the same file, skips it by name and panics on any other. -**Evidence.** With `issues/xhci-waits-are-spins.md` moved out of the -worktree, `cargo test --lib every_committed_binary_file_is_declared` exits 0. +**Evidence.** With a tracked issue file moved out of the worktree, `cargo test --lib every_committed_binary_file_is_declared` exits 0. A declared file that stops reading still reds, as a stale row. What passes is an undeclared path; by `std::fs::read`'s contract and not measured, a second diff --git a/issues/the-device-boot-is-flashed-by-no-metal-row.md b/issues/the-device-boot-is-flashed-by-no-metal-row.md new file mode 100644 index 00000000000..3666d82b2c1 --- /dev/null +++ b/issues/the-device-boot-is-flashed-by-no-metal-row.md @@ -0,0 +1,18 @@ +--- +status: open +kind: finding +opened: 2026-10-21 +--- + +# The device boot is flashed by no metal row + +`tests/images/metaldevicecase.toml`, the boot whose `usbwrite`, `usbread`, +`fbcheck`, `fbfill` and `fbread` jobs measure the stick and the framebuffer +(`tests/common/devices.rs`), was ridden by one row, `usb_reset_hands_devices_back`, +which went with the kernel's xHCI (the small-kernel track's step 10). No row +names its boot now, so its jobs' timings are taken on no machine; only its +config's job list is still checked (`the_config_runs_exactly_these_jobs`). + +**Exit**: a row that rides the device boot and reports its jobs, or the boot +and its jobs deleted. Owner: `tests/toyos.rs`'s metal profile, held by the +orchestrator, which schedules the benches' boots. diff --git a/issues/the-global-pipe-lock-spans-a-user-copy.md b/issues/the-global-pipe-lock-spans-a-user-copy.md index d0972d18e96..f44947fcc7d 100644 --- a/issues/the-global-pipe-lock-spans-a-user-copy.md +++ b/issues/the-global-pipe-lock-spans-a-user-copy.md @@ -26,7 +26,7 @@ Everything pipe-shaped in the kernel goes through the same static: `create` (`pi ## Impact, stated at its real size -This is **not** the device-I/O class. The copy is memory-speed and takes no device round trip, so it does not approach `sync::Lock`'s 500,000,000-spin `DEADLOCK` panic (`sync.rs:72-75`) the way a `block::Handle`'s device lock does — that class is tracked in `issues/xhci-waits-are-spins.md` and is not what this file is about. +This is **not** the device-I/O class. The copy is memory-speed and takes no device round trip, so it does not approach `sync::Lock`'s 500,000,000-spin `DEADLOCK` panic (`sync.rs:72-75`) the way the kernel's block device lock did before the block layer left the kernel — and that class is not what this file is about. What it is: a global lock held with preemption disabled for a stretch that scales with a userland-chosen length up to 2 MiB, on the path that carries every IPC message in the system. The scheduler's own bar for an uninterruptible stretch is `MAX_PASS_NS = 200_000` ns (`kernel/pure/sched/cpu.rs:1235`); a 2 MiB copy plus, on first write, a 2 MiB zeroing and a bitmap scan is on the wrong side of that at any plausible memory bandwidth. **The hold time is not measured and that is the first thing owed.** diff --git a/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md b/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md index 51cd48e6c5b..66cb1858668 100644 --- a/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md +++ b/issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md @@ -119,66 +119,30 @@ and NVMe. Exit: as stage A. `issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`, stage 3's module tests hold the bytes a file server ended under its clients had flushed, and stage 4 owes the client's `Gone` and reconnect, and the supervisor's restart, its bound and its refusal by name of a - restart whose claim is held, as module tests. The kernel's `AlreadyExists` on a held partition - is `partition_claim`'s. + restart whose claim is held, as module tests. - metal `layout_fresh_boot`: `/home`'s layout on the disk. - metal `pkg_install_gbae`: `pkg install` lands a package that runs. -- metal `partition_claim`: a partition claimed as a device, and its refusals. - metal `kernel_log_file`, `log_partition_layout`, `log_partition_identity`: `/log` on the stick and its layout. - metal `file_mtime_survives_a_reboot`: a file's mtime survives a reboot. ## Stage E: USB and xHCI -Host: `toyos-xhci` and its sim. Metal: the T14's controller and stick. Exit: as stage A. - -- host `xhci_many_devices`, `xhci_second_controller`, `xhci_two_controllers`: devices across one - and two controllers enumerate and deliver. -- host `xhci_msi_only`, `xhci_no_interrupt`: a controller with MSI only, and with no interrupt. -- host `xhci_slot_exhaustion`, `xhci_scan_hands_over_a_free_slot`: slots run out by name and are - handed back. -- host `xhci_deaf_registers`, `xhci_slow_connect`, `xhci_portsc_rw1c`, `xhci_flap`: registers that - never answer, a slow connect, PORTSC's write-one-to-clear, a replug inside the debounce. -- host `xhci_superspeed_ports`: device speeds bind to their ports. -- host `usb_storage_shapes`, `usb_refused_disk_first`, `usb_pool_exhausted`: disk sizes, sector - sizes and counts the driver serves or refuses. -- host `usb_storage_write_error`, `usb_flush_optional`: a refused write and a missing cache flush. -- host `usb_stick_left`: a stick that leaves its port while the port-reset rung holds it, at each - of three points (before the reset's completion is read, after it, and before the rung's TEST - UNIT READY), ends the run as the stick leaving: never `Rung::Offline`, no second break counted, - no Reset Device or Address Device sent to a port the reset or the TEST UNIT READY read empty, - and its slot disabled. Its row arms only `usb-transport-break`, and the T14 cannot pull its own - stick. Exit: a `toyos-xhci` sim test per point, red on each fact alone: when the empty port - climbs the ladder to `Offline`, when it counts a second break, when the sim's port is sent a - Reset Device or Address Device after it read empty, and when the stick's slot is never - disabled. -- metal `usb_reset_records_the_phase_it_cut`: a machine stopped inside a Bulk-Only command at - each of `DataOwed`, `Data` and `StatusOwed` resets itself, and the account the next pass reads - names that phase. Its row arms only `usb-reset-under-load` and reads whichever phase the sweep - cuts. Brings back `usb-wedge-data-owed`, `usb-wedge-in-data` and `usb-wedge-before-status`, - which meet `issues/a-deliberate-wedge-inside-a-driver-panics-every-other-cpu-that-wants-its-lock.md` - there. Exit: a row per phase, red when `stop::OpenCommand` publishes nothing. -- host `usb_reset_records_the_phase_it_cut`: `usb-reset-under-load` on a disk smaller than - `SWEEP_FLOOR` refuses by name and sweeps nothing; the T14's stick has the room. Exit: a host - test over the sweep's span lifted out of `kernel/src/usb_gate.rs`, red when the floor goes. -- metal `usb_reset_hands_devices_back`, the three resets its row does not reach, each judged on - the account in `loader.log` that `devices::quiesced` reads. Its row judges two orderly - reboots. - - The test runner's job deadline, with `quiesce-late-word` on `tests/jobdeadlinecase`: the stop - took the controller lock before the log volume's flush and completed. Exit: red when - `stop::settle_commands` is reverted. - - The panic console's bound, with `test-late-panic` and `panic-reboot-fast`: no barrier taken, - nothing flushed, and the stop still complete. Waits on - `issues/the-metal-loop-cannot-judge-a-boot-that-panics.md`, whose loop refuses that - boot. Exit: as the deadline's. - - The negative control, a controller lock that never comes free (`xhci-lock-wedged`): the - machine hands itself back anyway, and the account says the lock was not free inside its - bound. Exit: red when the barrier's bound is removed, which leaves the machine wedged. -- metal `usb_storage_gate`: the stick is read and written byte for byte. -- metal `late_storage_connect`: a disk that connects after the boot scan is bound, and `/boot` and - `/log` mount off it. Nothing is plugged: `xhci-slow-storage-connect` reports the first root-hub - port empty until the scan has run, and a row whose stick is on another port is red, not vacuous, - because its boot then logs no `usb-storage: 0 device(s)`. +The kernel drives no USB; usbd does (the small-kernel track's step 10). Host: +`toyos-xhci`'s stub tests, the driver against an xHC written from xHCI 1.2, +and `toyos-usbhid`'s and `toyos-scsi`'s decoders. Metal: the usbd rows. +Exit: as stage A, with what is still owed: + +- metal: the rewritten `usbload` row, a stick written continuously through + usbd and the machine reset under it by the boot deadline, judged on + `boot.usbload.stick_secs` (#901 section 9). +- metal: `tone_beside_a_stick_write` + (`issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md`). +- metal `late_storage_connect`: a disk that connects after bring-up is + served, and `/boot` and `/log` mount off it — on the stub, a device plugged + in later runs the same path as one present at bring-up + (`a_device_plugged_in_later_runs_the_same_path_as_one_present_at_bring_up`); + no bench has a stick connected late. ## Stage F: input and the i8042 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 bd4e0f1a27e..7098b4a2b71 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 @@ -8,9 +8,8 @@ opened: 2026-09-25 Owner ruling, 2026-09-25: the kernel is to be super small, super performant and safe. This track holds that, and supersedes the ordering of -`issues/every-driver-is-still-in-the-kernel.md` and -`issues/a-disk-plugged-in-after-boot-is-bound-inside-a-scheduling-pass.md`, -which stay as the evidence each stage closes. Owner: the orchestrator, which +`issues/every-driver-is-still-in-the-kernel.md`, which stays as the evidence +each stage closes. Owner: the orchestrator, which dispatches each step and each test this track owes. An exit holds the kernel's part of a restart, and named module tests hold each program's (#890). @@ -25,8 +24,9 @@ kernel's part of a restart, and named module tests hold each program's 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). -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. +4. **Step 10's metal rows still owed** (step 10, below): the rewritten + `usbload` row, `tone_beside_a_stick_write` and `mask_windows` under a stick + write. 5. **Stage 6 step 5, then stage 5's windows.** Beside that order, each waiting on something outside it: @@ -46,13 +46,11 @@ Beside that order, each waiting on something outside it: **Exit**: the machine boots in QEMU and on the T14 with the kernel's NVMe and USB storage paths never touched before the supervisor runs. The boot-time cost on the stick and on NVMe is measured. - **Partly done** (#506; T14 run 143 green), the stick's cost measured. - **Left**: `kernel_main` runs `xhci::init` and - `gpt::probe_usb_disks` after that spawn and before `smp::set_ready`, so - before the supervisor's first instruction - (`issues/storage-still-comes-up-before-the-supervisor-runs.md`); both go - with the kernel's xHCI at step 10. The NVMe cost waits on the self-update - track (above). + **Partly done** (#506; T14 run 143 green), the stick's cost measured, and + with step 10 the kernel drives no storage at all: no storage command is + issued before the supervisor runs, since the supervisor starts every disk + driver. **Left**: the NVMe cost, which waits on the self-update track + (above). 2. **One way to wait.** **Done** (#513). 3. **Storage in userland.** NVMe and USB mass storage are programs that claim their device through its IOMMU domain, as netstack does, and serve a block @@ -72,11 +70,12 @@ Beside that order, each waiting on something outside it: session reopened over the page a killed server left, by fileserver's `a_directory_rename_is_whole_under_one_name_wherever_the_server_is_killed` for DATA, and for LOG by the FAT test step 10 owes. **Partly done** - (#525, #536): NVMe is driven from userland, and the kernel's NVMe - driver is gone; the `nvme` driver offers its disk to diskserver, the block - service. **Left**: on the T14, `/log` and `/home` are on the - stick, which the kernel drives until step 10; the kernel's USB storage, - block layer, page cache and refusal chain go at step 9. + (#525, #536, #971): NVMe and the stick are driven from userland, and the + kernel's NVMe driver, USB storage, block layer and refusal chain are gone; + the `nvme` driver and usbd offer their disks to diskserver, the block + service. **Left**: the page cache, with step 9's file half, and the client + test `issues/nothing-reopens-a-diskserver-session-over-a-used-page.md` + owes. 4. **Where the data filesystem lives** — owner ruling, 2026-09-26, from the roasted proposal: userland file servers, the VFS a client library, and a program's view a set of directory capabilities the supervisor hands it, @@ -102,7 +101,7 @@ Beside that order, each waiting on something outside it: refused by name, red on a supervisor that starts the role again while its claim is held or that drops the name from the `AlreadyExists` refusal (`refused` in `system/supervisor/src/main.rs`). - **Partly done**: steps 1, 2, 3, 6 and 7 are done, and 4, 5, 8, 9 and 10 + **Partly done**: steps 1, 2, 3, 4, 6 and 7 are done, and 5, 8, 9 and 10 partly. Stages 3 and 4 are built as these steps: @@ -112,13 +111,11 @@ Beside that order, each waiting on something outside it: 4. **Partitions in diskserver**. **Exit**: a partition held by one session at a time, the idle slot claimed by GUID and written through diskserver, and `SYS_PARTITION_READ/WRITE` deleted once no disk the - kernel drives serves a claim. **Partly done** (#525, #826): sessions, a - block connector that opens only the partitions its badge grants, and - the idle slot on diskserver's disk written through one. **Left**: on a - disk the kernel drives, the stick among them, the supervisor still - claims a role's and the idle slot's partitions from the kernel, and - `SYS_PARTITION_READ/WRITE` serve them; both go with the kernel's USB - storage at step 10. + kernel drives serves a claim. **Done** (#525, #826, #971): sessions, a + block connector that opens only the partitions its badge grants, the + idle slot written through one, and the stick's partitions diskserver's + through usbd; the kernel drives no disk, and `SYS_PARTITION_READ/WRITE` + are deleted. 5. **Spawn and dlopen from an image handle.** **Exit**: a program in `/apps` runs having been read by userland, its faults served from the image object and not the VFS. **Partly done** (#536): spawn @@ -142,8 +139,7 @@ Beside that order, each waiting on something outside it: whole, shown red on an `open` whose truncate makes a new file under a held file's name. 7. **fileserver for LOG and BOOT; logkeeper on the client library.** - **Done** (#536); on a disk the kernel drives, fileserver opens LOG and - BOOT through the kernel's partition claims, which step 10 deletes. + **Done** (#536, and on the stick #971). 8. **Views as capabilities**, the isolation track's stage 1 (`issues/every-program-sees-only-the-files-it-was-given.md`). **Exit**: that stage's, run against fileserver: an escape suite red on @@ -172,104 +168,68 @@ Beside that order, each waiting on something outside it: `SYS_DLOPEN` (55) is step 5's. - **The block half**, with step 10: the block layer (`block.rs`), GPT (`gpt.rs`), USB storage (`drivers/usb_storage.rs`), the refusal chain - and `SYS_PARTITION_READ/WRITE`, all of which serve the stick until - usbd does. **Exit**: `git grep -n -e BudgetExpired -e DEADMAN -e - between_attempts -e until_answered -- kernel` finds nothing. + and `SYS_PARTITION_READ/WRITE`. **Exit**: `git grep -n -e + BudgetExpired -e DEADMAN -e between_attempts -e until_answered -- + kernel` finds nothing. **Done** with step 10. Each half measures the kernel's lines and its longest interrupts-off and preemption-off windows, by stage 6 step 2's rule. 10. **usbd**, stage 5's second half, and stage 4 of `issues/a-device-function-is-driven-from-its-arrival-to-its-end.md`: - usbd is a `drives` row, one process per xHCI controller the kernel - leaves. The whole xHCI moves, HID delivered - to the owner of input over the `input` port, each device its own - `source` with its own `gone`, and each stick a disk offered to the block - service on `disk`, whose sessions usbd serves over `toyos-blockring` + usbd is a `drives` row, one process per xHCI controller. The whole + xHCI is usbd's: HID delivered to the owner of input over the `input` + port, each bound collection its own `source` with its own `gone`, and + each stick a disk offered to the block service on `disk`, whose + sessions usbd serves over `toyos-blockring` (`issues/a-device-function-is-driven-from-its-arrival-to-its-end.md`'s - block ruling, which replaces #901's `block@` ports). A USB keyboard's transitions never reach the - kernel, whose keyboard and mouse streams go when their last kernel - source leaves (the ruling on #899, applied to usbd on #901). It - depends on stage 7 step 2 landing first, because at step 10 usbd's - is the kernel's last input source (the ruling on #904, - https://github.com/ToyOSOrg/ToyOS/pull/904#issuecomment-6139079945). - Its decoders are `toyos-xhci`'s Bulk-Only and SCSI modules and - `toyos-usbhid`. **Exit**: the kernel's part, usbd's claims handed - over again at its death: the controller's by `usbd_drives_the_spare` - on the default `qemu-xhci` (below) and - `usbd_drives_the_type_c_controller`; there is no keyboard claim. The - kernel's input streams, deleted: `keyboard.rs`, `mouse.rs` and - `input_merge_test.rs` gone from `kernel/src`, and no `Keyboard` or - `Mouse` in `sdk/abi`'s `DeviceType` or `FileType`. The - owner of input's, a key usbd held at its death released: - `toyos-input`'s test that a connection's end releases every device it - carried and a `gone` only its own `source`, red with `Merge`'s release - on a connection's end removed, and red with `gone` releasing its whole - connection. usbd's, a device that stops answering - costing only itself: `toyos-xhci-sim`'s - `a_controller_that_never_answers_still_releases_the_port`, - `a_recovery_the_controller_refuses_lets_the_device_go` and - `no_sequence_breaks_an_invariant`, with one sim test this step owes: - a stick whose bulk endpoint never answers leaves a keyboard on the - same controller delivering every report, red on a usbd that waits on - the stick before it reads the keyboard's ring. `/log`'s, what a killed - usbd had on the wire: stage 3's `toyos-blockring` tests for the - transport, and for usbd's session open `wire::Opened::over`, moved with - the open, and the client test + block ruling, which replaces #901's `block@` ports). **Done**: the + kernel's xHCI, USB storage, block layer, GPT, keyboard and mouse + streams, partition claims and `sync::OwedLock` are deleted, with the + xHCI and USB actuators. Every guest boot's stick goes through usbd, + and so does the HID of an image whose usbd receives `input` + (`images/console.toml`'s, `a_usb_key_reaches_the_console`); a test + image's usbd receives no `input`, so its HID is bound and let go at + the first `NotGiven`. The machine's stop has usbd reset every connected + port and its controller (`Hc::stop`) before the kernel's, as the + kernel's xHCI stop did. **The exit's evidence**: the kernel's + part by `usbd_drives_the_spare` on the default `qemu-xhci` and + `usbd_drives_the_type_c_controller`, and `usbd_serves_the_boot_stick` + for the boot controller; the kernel's input streams gone, with no + `Keyboard` or `Mouse` in `sdk/abi`'s `DeviceType` or `FileType`; the + owner of input's by `toyos-input`'s + `a_connections_end_releases_every_device_it_carried_and_a_gone_only_its_own_source`; + usbd's by `toyos-xhci`'s stub tests, under the names the exit gave + them — `a_controller_that_never_answers_still_releases_the_port`, + `a_recovery_the_controller_refuses_lets_the_device_go`, + `no_sequence_breaks_an_invariant` — and the owed + `a_stick_whose_bulk_endpoint_never_answers_leaves_a_keyboard_on_the_same_controller_delivering_every_report`; + `/log`'s by the `toyos-blockring` and `fat.rs` tests it named, and by + `the_log_lands_on_the_boot_stick` on QEMU's stick; the stop's by + `the_stop_resets_every_connected_port_then_halts_resets_and_unpowers_the_controller` + and the same row's judge of usbd's exit; the HID path's on a real xHC + by `a_usb_key_reaches_the_console`. + A machine whose units cannot remap refuses every claim, usbd's + included, so it drives no USB and its stick receives no `/log`: the + metal rows that asked for that machine (`iommu-no-remap`) are deleted, + and the refusal is QEMU's to hold, by + `usbd_refused_without_amdvi_remapping` and `amdvi_firmware_left`. + **Left**: the metal rows, which have not run: `usbd_serves_the_boot_stick` + and #901 section 9's three that judge with a userland load, + `stick_survives_a_reset_under_load` (a deadline reset under + `stick_load`), `tone_beside_a_stick_write` + (`issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md`'s + exit) and `mask_windows` under `stick_write`. #971's first metal boots + came back with no `logkeeper` file on the stick, which every metal + row reads; QEMU's stick boot (`the_log_lands_on_the_boot_stick`) has + it. And the client test `issues/nothing-reopens-a-diskserver-session-over-a-used-page.md` - owes; with one test this step owes in - `system/fileserver/src/fat.rs`: a disk stopped after each block write - in turn of an append and its sync mounts at every stop and reads back - all the last whole sync held, red on a sync that writes a growing - file's directory entry before its chain. + owes, which `/log`'s evidence names. - **Partly done** (#827), its stage 1: the test-only - `xhci-leave=:` leaves one controller to a claim, and - `/system/bin/usbd` drives it — the handoff, a reset, its rings in one - grant, MSI through the claim, and every device enumerated and named - over `inspect`; no class is bound, and no shipped image runs it. - **Left**, first P1 (order item 4), then the whole xHCI, one usbd per - controller (the orchestrator's stage-0 ruling). pcidev today arms MSI - only on a function with no MSI-X, and hands the BAR holding the - table over like any other where the machine remaps - (`kernel/src/pcidev/mod.rs`). QEMU's xHCI keeps its MSI-X table in - the BAR that holds the registers; QEMU's spare today is - `qemu-xhci,msix=off,msi=on` (`XHCI_SPARE` in `tests/common/qemu.rs`). - P1 serves usbd when that spare is `qemu-xhci` with its MSI-X left on - and `usbd_drives_the_spare` passes on it. The T14's xHCI has no MSI-X - on its capability walk, whose end is not read - (`issues/every-driver-is-still-in-the-kernel.md`). - - Read on the T14 (stage 0, `boot:testcases` readbacks): firmware owns - neither controller. 00:0d.0, 8086:9a13, the Type-C one (port 1 USB - 2.0, ports 2..=5 USB 3.1) and 00:14.0, 8086:a0ed (ports 1..=12 USB - 2.0, 13..=16 USB 3.1) each read `USBLEGSUP 0x01002201` and - `USBLEGCTLSTS 0xe0000000` — firmware's semaphore clear, every SMI - enable clear, the three status bits latched — and each is armed on - MSI. Neither 00:0d.0, whose release - `usbd_drives_the_type_c_controller` reads as `reset by nothing - (Express: no capability; AF: no capability; PM: No_Soft_Reset set)`, - nor QEMU's `qemu-xhci` advertises a reset, so a claim on either - inherits the last holder's ranges as residue; which reset 00:14.0 - advertises no boot has printed. - - Two compromises until this step, owned by it, whose exit is the - kernel's copy deleted with the kernel's driver. - `toyos-xhci`'s `descriptor` and the kernel's `parse_config` - (`kernel/src/drivers/xhci/device.rs`) are two decoders of the same - device-chosen bytes with different refusal rules — the kernel ends its - walk on a zero length and clamps, the crate refuses a length below 2; - its `xhci-descriptor-selftest` actuator goes with it. - `toyos-xhci`'s `xecp` and the kernel's - `kernel/src/drivers/xhci/legacy.rs` each carry the extended-capability - walk, the firmware handoff's verdict and the SMI-off word; the - orchestrator ruled the kernel's copy is not moved onto the crate's, - since it goes whole here, and its `xhci-xecp-selftest` actuator goes - with it. 5. **USB by userland**, with discovery and recovery written once as straight-line code: step 10, and this exit. **Exit**: step 10's, and no interrupts-off window longer than a register access, read by stage 6 step - 2's instrument. **Partly done**: step 10's stage 1 (#827). **Left**: the - rest of step 10, then this exit. + 2's instrument. **Partly done**: step 10. **Left**: step 10's metal rows, + then the windows. 6. A handler posts its device's `Watch` and ends its interrupt; the thread waiting on that watch does the work, and no step creates a kernel thread. `irq_ring`, the driver list in `drain_irqs` and the idle loop's device @@ -307,14 +267,16 @@ Beside that order, each waiting on something outside it: 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 - them. **Exit**: step 10's. **Not started.** + them. **Exit**: step 10's. **Done** with step 10. 5. **The pass is the scheduler's.** `drain_irqs` goes, and the TCO feed stays on the pass, since what it proves is that passes run. **Exit**: `drain_irqs` and the idle loop's device checks are gone, both windows are measured against step 2's readings by its rule, and the exits of `issues/an-irq-watchs-freeing-cancel-compiles-in-a-handler.md` and `issues/nothing-fails-when-a-devices-release-or-close-stops-answering-its-polls.md` - are met. **Not started.** + are met. **Partly done**: `drain_irqs` went with step 10, whose xHCI + was the last driver it polled. **Left**: the windows and the two + issues' exits. 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. **Done**. diff --git a/issues/the-key-record-the-surface-protocol-carries-is-defined-in-the-kernel-abi.md b/issues/the-key-record-the-surface-protocol-carries-is-defined-in-the-kernel-abi.md new file mode 100644 index 00000000000..87a31fccdbb --- /dev/null +++ b/issues/the-key-record-the-surface-protocol-carries-is-defined-in-the-kernel-abi.md @@ -0,0 +1,20 @@ +--- +status: open +kind: finding +opened: 2026-10-21 +--- + +# The key record the surface protocol carries is defined in the kernel ABI + +`RawKeyEvent`, the `MOD_*` flags and the pointer kinds live in +`sdk/abi/src/input.rs`, the kernel/program boundary, and the kernel no longer +speaks any of them: its keyboard and mouse streams went with the small-kernel +track's step 10. Their readers are the owner of input's wire +(`lib/input/src/wire.rs`), the surface protocol (`sdk/toyos/src/surface.rs`, +`sdk/window`) and the compositor's desktop. A userland protocol owns its wire +types in its own library (root `CLAUDE.md`), so these belong to the surface +protocol and `toyos-input`, not to `toyos-abi`. + +**Exit**: `sdk/abi` defines no input record, and the surface protocol and +`toyos-input` own theirs. Owner: `lib/input`, held by the orchestrator, beside +the input track's text (#904). diff --git a/issues/the-loader-does-only-what-must-precede-the-handover.md b/issues/the-loader-does-only-what-must-precede-the-handover.md index bebffcdc939..4c209db9f86 100644 --- a/issues/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/the-loader-does-only-what-must-precede-the-handover.md @@ -337,7 +337,6 @@ Each stage lands on its own, in this order. - It runs over the cable that `issues/the-t14-answers-only-through-a-usb-stick.md` owns, and waits on - `issues/a-reset-stops-xhci-and-leaves-every-claimed-pci-function-armed.md`, `issues/the-t14-hung-after-rebooting-with-its-i219-faulted.md`, `issues/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md`, `issues/most-t14-leases-land-one-dhcp-retry-late.md` and diff --git a/issues/the-supervisors-claim-of-a-pci-function-the-t14-lacks-holds-preemption-off-for-3-8-ms.md b/issues/the-supervisors-claim-of-a-pci-function-the-t14-lacks-holds-preemption-off-for-3-8-ms.md index aaaf13ef647..5b4153c9de8 100644 --- a/issues/the-supervisors-claim-of-a-pci-function-the-t14-lacks-holds-preemption-off-for-3-8-ms.md +++ b/issues/the-supervisors-claim-of-a-pci-function-the-t14-lacks-holds-preemption-off-for-3-8-ms.md @@ -42,8 +42,8 @@ On the head no CPU but the one `hold_once` held reads more than 44537 ns of interrupts off in that report, which bounds every window inside the claim from above; no reading separates the claim's own windows from the rest. -Before the first job's exit cpu0 also carries init's partition claims, 6.0 -and 6.6 ms in that boot (`issues/xhci-waits-are-spins.md`), and the +Before the first job's exit cpu0 also carried init's partition claims, 6.0 +and 6.6 ms in that boot, which left the kernel with its xHCI, and the `mask-windows` kernel (`kernel/src/windows.rs`) prints each CPU's longest window and no opener, so its cpu0 line there reads this claim only once those have left the kernel. diff --git a/issues/the-t14-hung-after-rebooting-with-its-i219-faulted.md b/issues/the-t14-hung-after-rebooting-with-its-i219-faulted.md index 2deccf2f194..365e3d63330 100644 --- a/issues/the-t14-hung-after-rebooting-with-its-i219-faulted.md +++ b/issues/the-t14-hung-after-rebooting-with-its-i219-faulted.md @@ -16,8 +16,8 @@ between the census at 54.000 s and the one at 54.430 s. Run 121's boot, which rebooted cleanly, took 2,244 between its last census and its own `Syncing filesystems...` 381 ms later (`t14-run122/sda3-before-flash.img`, the stick as run 121 left it): the same rate, because that span is the log's last -writes to the stick. `issues/an-xhci-storm-starves-the-cpu-that-takes-it.md` -is a different shape — ten timer interrupts in thirty-four seconds; here cpu0 +writes to the stick. The kernel xHCI's storm on run 20, closed with that +driver, was a different shape — ten timer interrupts in thirty-four seconds; here cpu0 took 43 in the same 430 ms. **What a clean reboot has and this one lacks** is everything after diff --git a/issues/the-t14s-superspeed-stick-enumerates-at-high-speed-under-toyos.md b/issues/the-t14s-superspeed-stick-enumerates-at-high-speed-under-toyos.md index 0ead40d522d..8038340d750 100644 --- a/issues/the-t14s-superspeed-stick-enumerates-at-high-speed-under-toyos.md +++ b/issues/the-t14s-superspeed-stick-enumerates-at-high-speed-under-toyos.md @@ -43,5 +43,5 @@ the stick leaves: that is the USB 3.x specification's device-side rule. ## Exit condition -A T14 boot whose kernel log shows the stick enumerated on the USB3 port of its -receptacle at the first scan, or the measurement that says why it cannot be. +A T14 boot whose usbd lines show the stick enumerated on the USB3 port of its +receptacle at bring-up, or the measurement that says why it cannot be. diff --git a/issues/there-is-no-wifi.md b/issues/there-is-no-wifi.md index be9ef18c6d7..d284a231280 100644 --- a/issues/there-is-no-wifi.md +++ b/issues/there-is-no-wifi.md @@ -143,13 +143,12 @@ controller through its table in BAR 0. - **Exit, owed on the benches:** - on the T14, `msix_table_read_in_place` reading the AX210's and the NVMe's armed MSI-X entries back through their holders' mappings at - BAR 0 + 0x2000, and `claim_reuses_its_remapping_entry` and - `claim_refused_without_remapping` unchanged; + BAR 0 + 0x2000, and `claim_reuses_its_remapping_entry` unchanged; - the T14's NVMe driven through that table: an Identify Controller admin command whose completion arrives through the in-place MSI-X entry, which touches none of the other system's data; no row does it yet; - on the AMD bench, `amdvi_usbd_drives_its_own_entry`: `03:00.4` - (`xhci-leave=03:00.4`, its MSI-X table in BAR 0, its only BAR, behind + (which no test image's usbd row names, its MSI-X table in BAR 0, its only BAR, behind `00:08.1`, whose `0xd0000000..0xd0500000` bus 3 fills) claimed in place and driven by usbd through its own remapping entry, every unit filtering SMIs: the `usbd_spare`, `usbd_spare_fault`, `end_returns_claims` and diff --git a/issues/toyos-records-carry-the-sticks-serial-the-cards-mac-and-the-resolvers-in-full.md b/issues/toyos-records-carry-the-sticks-serial-the-cards-mac-and-the-resolvers-in-full.md index 69861737926..cfc3e4a003f 100644 --- a/issues/toyos-records-carry-the-sticks-serial-the-cards-mac-and-the-resolvers-in-full.md +++ b/issues/toyos-records-carry-the-sticks-serial-the-cards-mac-and-the-resolvers-in-full.md @@ -8,11 +8,11 @@ opened: 2026-10-03 Nothing that identifies the owner's machines or network goes into the tree, a commit message or anything posted on GitHub (root `CLAUDE.md`). ToyOS prints -three such values in full on every boot of the T14, so every T14 log pasted in -public carries them unless whoever pastes it masks them by hand: +two such values in full on every boot of the T14, so every T14 log pasted in +public carries them unless whoever pastes it masks them by hand (the USB +stick's serial was a third, until usbd took the stick; usbd reads it to tell a +returning disk apart and prints it nowhere): -- the USB stick's serial number: `kernel/src/drivers/xhci/wait/msc.rs:1407`, - on every bind, and `:1415`, when a disk comes back; - the network card's MAC: `system/netstack/src/main.rs`, `main`; - the resolvers the lease named, which on the bench are the provider's public ones: `system/netstack/src/main.rs`, `Leases::pass`. diff --git a/issues/toyos-runs-in-any-open-source-hypervisor.md b/issues/toyos-runs-in-any-open-source-hypervisor.md index e0b979772ea..b6beda4b991 100644 --- a/issues/toyos-runs-in-any-open-source-hypervisor.md +++ b/issues/toyos-runs-in-any-open-source-hypervisor.md @@ -40,9 +40,6 @@ and, filed before this track: - `issues/a-machine-without-an-iommu-refuses-every-claim.md` — bhyve; crosvm for every device it emulates; and VirtualBox, QEMU and Cloud Hypervisor by default (`issues/no-claim-gets-an-address-space-under-virtio-iommu.md`). -- `issues/a-hid-interface-outside-the-boot-subclass-is-read-as-qemus-tablet.md` - — VirtualBox's USB tablet, a subclass-0 interface; bhyve's declares the - boot mouse subclass and protocol. - `issues/getsockname-answers-an-address-nobody-asked-netstack-for.md` — QEMU's user-mode network address in `sdk/libc`. - `issues/pci-is-enumerated-on-segment-group-0-alone.md` — Cloud diff --git a/issues/usb-audit-findings-not-in-vcs.md b/issues/usb-audit-findings-not-in-vcs.md deleted file mode 100644 index a74bb497c5a..00000000000 --- a/issues/usb-audit-findings-not-in-vcs.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-08-08 -task: 145 ---- - -# Three findings of the second mass-storage audit exist nowhere in VCS - -Filed out of the second USB audit entry when the rest of it closed — this is the only trace in the repository that they exist. - -F-D, F-F and F-I of the second USB mass-storage audit are recorded **only in -task #145's description**, file and line each. Writing them here from memory -would be inventing them, so whoever holds that task must paste them in. - -What is not owed: F-K is `with_storage`'s non-local invariant and is stated at -that function in `kernel/src/drivers/xhci/wait/msc.rs`, F-E is the EP0 recovery -path, and F-J — the file-cache error channel — is closed. diff --git a/issues/usb-disk-index-stable-nothing-enumerates-on-the-first-controller.md b/issues/usb-disk-index-stable-nothing-enumerates-on-the-first-controller.md deleted file mode 100644 index 77aedba3f46..00000000000 --- a/issues/usb-disk-index-stable-nothing-enumerates-on-the-first-controller.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-08-08 ---- - -# `usb_disk_index_stable` reds 1 of 5 on CI: nothing enumerated on the first controller - -From the twelve-shard CI probe (`probe-rate.yml` run `31258202923`, tree -`f8f73e1`, five reps of the exact `ci.yml` configuration): `usb_disk_index_stable` -red 1 of 5, shard 2, `Sched::Parallel`, `nothing enumerated on the first -controller`. Re-taken 2026-09-04 against the last three nightly `ci` runs on -`main` (`33485669019`, `33603832656`, `33728852421`): of the eleven names that -probe found, only this one still reds. - -`usb_disk_index_stable` is deleted; `4505f872d`'s parent restores it. - -**Exit condition.** The cause of the empty first controller is fixed. Owner: orchestrator. diff --git a/issues/usb-short-read-reds-beside-other-guests-and-is-green-alone.md b/issues/usb-short-read-reds-beside-other-guests-and-is-green-alone.md deleted file mode 100644 index 80888398f6a..00000000000 --- a/issues/usb-short-read-reds-beside-other-guests-and-is-green-alone.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-08-22 ---- - -# `usb_short_read` reds beside other guests and is green alone, and nobody has a rate for it - -Seen on 2026-08-22 in a full fast tier on the dev host (`main` 7129931d merged), -one run in three: - -``` -FAIL usb_short_read: one short read cost the disk the rest of its sweep - FAIL usb_short_read (5s) - ALONE usb_short_read: GREEN — it fails only beside other guests, so its - Sched::Parallel is wrong. The run stays red on the classification. -``` - -Re-run immediately after, alone, on the same tree and in the same session: -**green, 2 of 2**, 2.9 s each, host load average 7.70 with another agent's work -on the machine. So the observation is 1 red in 3 with no rate behind it. - -`ALONE: GREEN` is a hypothesis and not a finding (`tests/CLAUDE.md`): what is -owed is a rate measured in one session against an unchanged tree, not a -re-classification of its `Sched`. The verdict's own words — one short read -costing the disk the rest of its sweep — are about the driver's recovery after -`usb-short-read` injects the under-delivery, so a red here is worth a look at -whether the sweep's continuation depends on timing the host can stretch. - -Not the diff it was seen from: that branch changes `syscall_window_nmi`'s -assertions and two kernel doc headers, and touches nothing under `xhci/` or -`usb_gate`. - -**Its test is deleted**, as a flaky test is: `402107ba2` took `usb_short_read` -out with `usb-short-read`, the actuator only it armed, and `git revert -402107ba2` brings both back. - -**Exit**: a cause for one short read costing the disk the rest of its sweep -beside other guests, and the test restored and green beside them. diff --git a/issues/usb-transport-break-flushedstick-can-break-after-the-reboot.md b/issues/usb-transport-break-flushedstick-can-break-after-the-reboot.md deleted file mode 100644 index 6e9bb0f42c2..00000000000 --- a/issues/usb-transport-break-flushedstick-can-break-after-the-reboot.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-24 ---- - -# `usb_transport_break`'s FlushedStick case can stage its break after the boot's last word - -`usb-transport-break-flushed` breaks the first WRITE(10) after a write that -completed and a SYNCHRONIZE CACHE that succeeded over it. The case's job is -`reboot`, and nothing makes a write go out between the stage being armed and -the shutdown: when logd's writes before the job are all covered by flushes the -stage has not yet seen, the first write that qualifies is the one carrying -the shutdown's records, and the break lands after `Rebooting.`: - - [kernel 0.472 cpu3] Rebooting. - [kernel 0.473 cpu0] usb-storage: the WRITE(10) going out breaks next (usb-transport-break-flushed): a write was reported complete and a SYNCHRONIZE CACHE succeeded after it, with no write since - -and the judge says `FlushedStick: after the break, no line reads "Rebooting.", in order`. - -Measured on `origin/main` at `7a5a98d3` in a worktree of its own, alone: -red 1 of 4 `--nightly usb_transport_break` runs, green on the harness's own -re-run. The logd branch, its tree as committed at `ebf2f76a`, measured the same words -1 of 3. - -## Exit condition - -The case's premise is arranged rather than raced: a write after a verified -flush is made to go out before the job ends the boot (or the case waits on the -break before it starts the job), and ten consecutive -`usb_transport_break` runs are green. - -`usb_transport_break` is deleted; `issues/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md` records the commit that restores it. diff --git a/issues/values-no-document-states-stand-where-another-os-was-their-source.md b/issues/values-no-document-states-stand-where-another-os-was-their-source.md index 6d0dfc678cc..4867b8a3f72 100644 --- a/issues/values-no-document-states-stand-where-another-os-was-their-source.md +++ b/issues/values-no-document-states-stand-where-another-os-was-their-source.md @@ -9,7 +9,6 @@ opened: 2026-10-14 Each value below was set as Linux's and cited so. Its citation is gone, and no vendor document or published specification states the number; the behaviour is unchanged. -- `lib/xhci/src/ladder.rs`'s `RESET_RECOVERY_NS`, 50 ms. USB 2.0 §7.1.7.5 asks 10 ms (TRSTRCY). - `system/fileserver/src/resolve.rs`'s `MAX_LINKS`, 40. POSIX asks `{SYMLOOP_MAX}` of at least `{_POSIX_SYMLOOP_MAX}`, 8. - `system/netstack/shard/tcp/src/conn.rs`'s `PROBE_SLACK`, 2 ms, added to RFC 8985 §7.2's diff --git a/issues/volatile-composites-on-mmio-dma-structs.md b/issues/volatile-composites-on-mmio-dma-structs.md index 4e7cecf15ed..1d14eca2f44 100644 --- a/issues/volatile-composites-on-mmio-dma-structs.md +++ b/issues/volatile-composites-on-mmio-dma-structs.md @@ -8,12 +8,10 @@ opened: 2026-08-20 `clippy::volatile_composites` (nursery) measured **10**, kernel-only, all in driver code that hands a composite type straight to `write_volatile` or -`read_volatile` instead of touching each primitive field on its own. Two were -the NVMe driver's queue entries and one virtio-gpu's EDID answer, which left -the kernel with their drivers; the rest: +`read_volatile` instead of touching each primitive field on its own. The NVMe +driver's queue entries, virtio-gpu's EDID answer and the xHCI's `ErstEntry` +and `Trb` left the kernel with their drivers; what is left: -- `drivers/xhci/wait/boot.rs:446` — `ErstEntry`. -- `drivers/xhci/mod.rs:560,593,1093` — `Trb`, three sites. - `drivers/virtio.rs:414,487` — the virtio descriptor. The lint's own docs put the reason plainly: volatile ops are well-defined for diff --git a/issues/xhci-flap-wedges-under-kvm.md b/issues/xhci-flap-wedges-under-kvm.md deleted file mode 100644 index 0980fa2e53d..00000000000 --- a/issues/xhci-flap-wedges-under-kvm.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-08-08 ---- - -# `xhci_flap`'s fourth collapsed replug wedges the driver under KVM - -Run `31246245541`, `debian:sid`/QEMU 11.0.3/KVM, `--jobs 1`, alone: -`timed out after 164s`. Green under TCG on the same runner image and the same -QEMU, and green on the dev host. - -Three of the four cycles are in the log and all three are the state under test — -`port 5 was unplugged and plugged back in between two looks; tearing the old -device down before enumerating what is there now`, followed each time by a clean -re-enumeration and `pointer on slot 1 merges as source 2`, at 1.879, 3.084 and -3.787 s. Then nothing: the fourth `device_del`/`device_add` pair goes in at about -4.4 s and the guest never speaks again, never delivers the pointer motion the -test ends on, and `test_rs_input_events` therefore never exits. - -So the driver survives three collapsed replugs and stops on the fourth, on the -accelerator that runs the guest ~50x faster between the host's two QMP writes. -The owner's rule names what this is — "everything should work under emulation and -kvm if it doesnt something with the guest is wrong". Not diagnosed further -here; found by CI, which is the thing CI was built to do. - -**It has stopped reproducing, which is not the same as being fixed.** Run -`31258202923`, five reps of the whole twelve-shard configuration on the same -image and the same accelerator: `xhci_flap` is **PASS 5 of 5, in 7–9 s**. Nothing -in `toyos_xhci` changed between the two runs; what did change is `wt/toyos-clock`, -which replaced flat host waits with waits bounded by the guest, so one candidate -is that the probe's 164 s was a harness ceiling on a slow re-enumeration rather -than a wedge — and the log above, where the guest goes silent after the fourth -pair and never speaks again, is evidence against that reading. Left OPEN: a -defect that stopped appearing under an unchanged driver is a defect whose trigger -nobody has named. Re-read this entry from the log quoted above, never from a -green run. - -**And it is not alone.** Run `31247206462`, twelve shards on KVM at `--jobs 1`, -put four more of its shape on the list, every one red again when re-run alone: - -| test | what it does over QMP | alone | -|---|---|---| -| `xhci_hotplug` | `device_add`/`device_del`, fixed waits against a 100 ms debounce | `timed out after 66s` | -| `xhci_hid_break` | a staged transfer error on a HID endpoint | `timed out after 75s` | -| `metal_sim_pointer_churn` | 8 plug/unplug cycles under a live compositor | `bound 0 pointer sources` | -| `usb_transport_break` | one staged break, and the driver's first-try recovery | `the transport broke 2 times` | - -All four are green on the dev host in seconds and green under TCG on the same -runner image and the same QEMU. So the class is one sentence: **the xHCI driver's -plug, unplug and endpoint-recovery paths are unreliable when the guest executes -natively**, and the ~50x speed-up between the host's two QMP writes is the only -thing that changed. `issues/pulling-the-boot-stick-freezes-the-t14.md` names the shape it would be in — one -outstanding operation per controller, a completion matched by its Command TRB -address, a recovery cancelled by a disconnect rather than waited out. - -**This class is what stands between CI and green.** It is not -`issues/parallel-tests-red-under-other-suites.md`'s classification -class: it fails alone, and it fails the same way twice. - -**Three of the four have stopped and one has not.** Run `31258202923`, five reps -of the whole configuration on the same image and accelerator: `xhci_hotplug`, -`xhci_hid_break` and `metal_sim_pointer_churn` are **0 of 5** — the last of those -is closed, a console the test had counted before it caught up, and the other two -coincide with `wt/toyos-clock`'s waits — while -**`usb_transport_break` is 5 of 5** with the same sentence it has always given, -`the transport broke 2 times; the injection is armed once per boot, so anything -else is a break this test did not stage`. That is the one member of this class -that is still reproducible, and it is the one to take: it is not a ceiling, it is -the driver reporting a second break nobody staged. - -**Taken, and it was the driver.** Closed: the Bulk-Only Reset raced the transfer it was recovering from — see `reset_recovery` in `kernel/src/drivers/xhci/wait/msc.rs` and `lib/xhci/src/recovery.rs`. diff --git a/issues/xhci-waits-are-spins.md b/issues/xhci-waits-are-spins.md deleted file mode 100644 index fcfd563d8af..00000000000 --- a/issues/xhci-waits-are-spins.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-08-03 ---- - -# The xHCI driver's waits are spins, under a lock and with preemption off - -Every wait in the kernel's xHCI driver spins against a wall-clock deadline -while holding `XHCI` (`kernel/src/drivers/xhci/mod.rs`), a ticket spinlock and -so preemption off for its whole life: `settles()` for the controller's halt, -reset and port reset, and `wait_command()` and `wait_transfer()` for every -command and transfer, each bounded by `USB_TIMEOUT_NS`, 2 s. The port -machine's enumeration and HID recovery submit and return; what still spins is -the boot path's bring-up, the stop, a disk's bind after boot, inside a -scheduler pass -(`issues/a-disk-plugged-in-after-boot-is-bound-inside-a-scheduling-pass.md`), -and a disk call. - -A call on a USB disk holds `XHCI` from its first command (`with_disk`, -`kernel/src/drivers/xhci/wait/msc.rs`) inside a syscall, which runs with -preemption off from entry to exit and, since the gate opens them, interrupts -on (`issues/syscall-preemption-is-incidental.md`): a partition claim's -`SYS_PARTITION_READ`, `SYS_PARTITION_WRITE` and `SYS_FSYNC`, the last a cache -flush through `xhci::storage_flush` (`partition_fsync`, -`kernel/src/object/ops.rs`), and the table `SYS_DEVICE_CLAIM` reads for one -(`gpt::claimable`, `kernel/src/gpt.rs`). logd's `fsync` is one of these: the -LOG fsd answers each with `SYS_FSYNC` on its claim. So its CPU holds -preemption off for as long as the device takes. A disk's bind after boot -spins inside the Ring 3 tick's pass, which runs with interrupts masked, and -`time::DEAF_CPU` (5 s), past which a CPU waiting on a TLB acknowledgement -panics, is held above `CALL_AFTER_BREAK` (4.75 s), the longest one call spins -once its transport has broken. - -Its "On the T14" readings below were taken while a syscall still ran with -interrupts masked from entry to exit. - -## On the T14 - -On LENOVO 20W0003AMZ, BIOS N34ET71W (1.71), `/log` is a FAT32 partition of the -USB stick the machine boots from, which `/system/bin/fsd` in the LOG role -writes with `SYS_PARTITION_WRITE`. One boot of `main` at `c59e09ed6` with a -throwaway instrument that records every window of 2 ms or more with the -addresses that opened and closed it, its CPU's last events, and samples of -that CPU every 2 ms ran `test_rs_ring_park_herd` forty times. Its lines are -quoted on #681 (comments 5968784527, 5962618492 and 5962618868). Pid 4 is that -fsd. - -- **Eleven writes of 7.8 ms or more in 37 s**, 2.0 to 4.6 s apart, each window - opened at the syscall's entry and closed at its return. Nine are on one CPU - alone: 8,289,211 to 10,669,042 ns. 36 of their 37 samples are in - `wait_transfer < bulk < bot < scsi < transfer_blocks` under `storage_write`; - the 37th is the same syscall waiting for the process table's lock. -- **All eight CPUs at once, twice**, at 17.416 s and 21.116 s: the write on one - CPU (7,848,573 and 7,927,725 ns) while a herd thread on each of the other - seven is in `SYS_THREAD_EXIT`, whose unmap shoots down every CPU's TLB. The - fourteen shootdowns span 7,703,612 to 7,852,515 ns, and their initiators - spin: none goes more than 10,819 ns between two turns of its wait. The other - targets acknowledge within 24 µs; the writing CPU acknowledges by interrupt - once its syscall has returned, 130 to 264 ns before each shootdown ends. Each - event is followed at once by a shorter one of the same shape: the next - write, 2,824,878 and 2,846,782 ns, and fourteen more shootdowns. The - report's `tlb:` line reads `max=7846us` after the first, where it read - `max=1146us` before it. -- **Shorter**: sixteen of the same server's writes read 2,368,371 to - 2,905,716 ns, those two among them, and two of its `SYS_PARTITION_READ` - 2,596,003 and 2,954,780. At the stop, the black box keeps a write opened at - 38.795 s whose CPU's next event comes 7,953,409 ns later. -- **init's partition claims**: on cpu0, 6,002,063 ns at 1.171 s and - 6,623,907 ns at 1.179 s, each `SYS_DEVICE_CLAIM` from entry to return, all - four samples in `wait_transfer` under `storage_read`. -- **Its `SYS_FSYNC`**: 15 windows' trails carry one, and in 7 it spins on a - contended `XHCI` at `with_disk_by` (comment 5966523482). None held - interrupts off for 2 ms. -- **Not the firmware**: `MSR_SMI_COUNT` does not move across any of them. The - windows it does move across are - `issues/the-t14s-firmware-interrupts-every-cpu-every-2-2-s-under-toyos.md`'s. - -The `mask-windows` kernel (`kernel/src/windows.rs`) prints a span and no -opener, and #649's T14 boots printed these by reading: - -- cpu0's line before the first job's exit, init's partition claims: - 6,540,326 to 6,590,095 ns in seven of the eight `mask_windows` boots at - `72f16e39a`, and 6,533,222, 2 × 3,273,547 and 6,508,165 in the first report - of the three at `8b73eba69` (comment 5959415453). -- Every CPU at once: 9,603,617 to 9,651,846 ns beside `tlb: … max=9575us`, the - fourth boot at `72f16e39a`; 2 × 5,014,552 to 2 × 5,075,290 beside - `max=10038us`, `649-r6/2-report-halved` (comment 5960575031). -- One CPU alone: 9,260,193, the seventh boot at `72f16e39a`; cpu7, where pid 4 - started, at 11,492,028 and 8,456,658 before the first job's exit - (`649-r6/1-head`, `649-r6/3-idle-halt-counted`). -- The stop's report: one CPU at 10,214,820 to 11,267,384 in four boots at - `72f16e39a`, and two in `649-r5/1-head`, cpu7 at 11,522,834 and cpu4 at - 11,645,411 (`loader.log:57`, `:63`). - -**Owner**: `issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`, -whose step 10 moves the whole xHCI to usbd and deletes the kernel's driver. It -owes both exits. - -**Exit**, two: -- **The tripwire's, short of step 10**: no disk wait spins with `IF` clear, - because the tick's pass leaves the interrupt gate (step 2 of - `issues/toyos-beats-linuxs-latency-on-the-t14.md`), and - `kernel/src/drivers/xhci/mod.rs` holds no bound under `DEAF_CPU`. -- **The spin's**: the tree has no `kernel/src/drivers/xhci`, so no kernel wait - is a USB device's. Step 10 meets both. diff --git a/kernel/CLAUDE.md b/kernel/CLAUDE.md index 5af7dee15b6..8132c326b18 100644 --- a/kernel/CLAUDE.md +++ b/kernel/CLAUDE.md @@ -9,10 +9,7 @@ The module header at the site owns its subsystem — read it before changing a m - **The idle loop may not take a global lock unconditionally** — the crash report reads the process table through a `try_lock` it must never block on. `sched/reap_gate.rs` is the pattern: a relaxed-load gate in front of the lock. - **Every x86 Ring 0 entry clears the direction flag** — no hardware gate does it, and `memmove` sets it across `rep` operations with interrupts on; `arch::entry::ring3_naked_asm` is where it lives. - **`BackendGuard` masks interrupts for its whole life**, so anything written under it is an interrupt latency; a new holder bounds itself as the console drain and the userland `write` flush do. -- **No disk wait in this kernel can park** — a transfer is waited for under ticket spinlocks, each disabling preemption. -- **`drain_irqs` is the drivers' engine and nothing on it may wait** — a blocking call there empties the audio pipeline on every plug. - **A syscall that can block resolves its handle and clones the object out before it blocks** — a `with_object`/`with_process_data` guard held across a park is a runtime panic no compile check catches; the `SYS_FSYNC` arm in `syscall/dispatch.rs` is the pattern. -- **A block-layer `BudgetExpired` is not-durable-yet and never a loss** — it is retried on a fresh budget above every lock; a flush that discards its pages on one splits a FAT mirror. - **A decision the process table makes lives in `kernel/pure/proclife/`, never in `process.rs`** — its defects are interleavings and its host tests are the only machine that can enumerate one. - **A task holds at most one watch registration** — a standing registration across a loop must not call anything that registers again. A double registration panics only at attempt ≥ 2, so the contention depth is the coverage. - **A console is per holder, minted at spawn** — the object *is* the line buffer. diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 36c68e0cb79..8c5c647ebbc 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -39,7 +39,7 @@ poll-fire-load-store = [] # and `kernel/loom/tests/inbox_answer.rs` must red — a post with nothing to # read, answered. post-is-an-answer = [] -# The same arrangement, five more times, one per `kernel-loom` model that had +# The same arrangement, four more times, one per `kernel-loom` model that had # no control of its own until 2026-08-17. Each is declared here and never # enabled here; `kernel-loom` turns exactly one on at a time and the named # model must red. None costs a kernel build — no kernel `cargo test` @@ -47,9 +47,6 @@ post-is-an-answer = [] # # `src/sync.rs`'s two loads of `now` go `Relaxed`, and `ticket_lock` reds. lock-acquire-off = [] -# `src/sync.rs`'s `owed_fence` is dropped from both sides of `OwedLock`, and -# `owed_lock` reds. -owed-fence-off = [] # `src/sched/reap_gate.rs`'s `raise` store goes `Relaxed`, and `reap_gate` # reds. reap-raise-relaxed = [] @@ -365,13 +362,10 @@ heap-band-nohead = ["heap-tripwire"] # `sched::driver`'s `check_stack_ownership` compares, at every pass, the two # words that aim a Ring 3 entry at a stack — `percpu.kernel_rsp` and # `tss.rsp0` — against the running task's own stack top, and this CPU's `rsp` -# against that stack's bounds; `drivers::xhci::wait::msc`'s `BlockWitness` -# snapshots the one field that has been seen changing inside a single call and -# names the frame, the depth and the value when it does. +# against that stack's bounds. # # Its own build and not a boot parameter, for `heap-tripwire`'s reason turned -# around: the driver half is a comparison on the block path and the scheduler -# half is three comparisons on the pass path, so a kernel carrying it is not +# around: it is three comparisons on the pass path, so a kernel carrying it is not # the kernel a rate was measured on and it has to be in *both* arms of any # comparison. It compiles no decision — every line is a reader and a panic. stack-witness = [] @@ -486,12 +480,9 @@ toyos-acpi = { path = "../lib/acpi" } toyos-amdvi = { path = "amdvi" } toyos-dma = { path = "dma" } toyos-blackbox = { path = "../lib/blackbox" } -toyos-blockhold = { path = "../lib/blockhold" } toyos-bootmap = { path = "../lib/bootmap" } -toyos-fat32 = { path = "../lib/fat32" } toyos-elf = { path = "../lib/elf" } toyos-gicv3 = { path = "gicv3" } -toyos-gpt = { path = "../lib/gpt" } toyos-its = { path = "its" } toyos-microcode = { path = "../lib/microcode" } toyos-pci = { path = "pci" } @@ -502,11 +493,8 @@ toyos-tsc = { path = "../lib/tsc" } toyos-rootimage = { path = "../lib/rootimage" } toyos-quiesce = { path = "../lib/quiesce" } toyos-random = { path = "../lib/random" } -toyos-scsi = { path = "../lib/scsi" } toyos-symbols = { path = "../lib/symbols" } -toyos-usbhid = { path = "usbhid" } toyos-userbound = { path = "../lib/userbound" } toyos-wallclock = { path = "../lib/wallclock" } -toyos-xhci = { path = "../lib/xhci" } hashbrown = { version = "0.16", default-features = false } dlmalloc = { version = "0.2", default-features = false } diff --git a/kernel/loom/Cargo.toml b/kernel/loom/Cargo.toml index cf5c7abc3e6..2b4df3d483f 100644 --- a/kernel/loom/Cargo.toml +++ b/kernel/loom/Cargo.toml @@ -66,17 +66,6 @@ post-is-an-answer = [] # Never on by default and never reachable from a kernel build, which declares # the same name only so `cfg` checking knows it. lock-acquire-off = [] -# The negative control for `OwedLock`'s handoff. It drops `sync.rs`'s -# `owed_fence` from an owing CPU's retry and from a release's look, so each can -# miss the other's write and a CPU is turned away with nobody to answer it, and -# `owed_lock.rs` must red: -# -# cargo test --manifest-path kernel/loom/Cargo.toml --features owed-fence-off \ -# --test owed_lock -# -# Never on by default and never reachable from a kernel build, which declares -# the same name only so `cfg` checking knows it. -owed-fence-off = [] # The negative control for the reap gate's enrolment edge. It makes # `reap_gate.rs`'s `raise` store `Relaxed`, so a claimer is handed a gate # without the work it was raised for, and `reap_gate.rs` must red: diff --git a/kernel/loom/tests/owed_lock.rs b/kernel/loom/tests/owed_lock.rs deleted file mode 100644 index 6056a58220b..00000000000 --- a/kernel/loom/tests/owed_lock.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Loom: `OwedLock`'s handoff. -//! -//! A CPU that `try_lock_or_owe` turns away halts on the work it left, so it -//! must come back: either its retry took the lock, or the holder's release saw -//! its bit and answered it. That is a store-buffer pair, an owing store then a -//! read of `now` against the release's store to `now` then a read of the owed -//! word. -//! -//! ```text -//! cargo test --manifest-path kernel/loom/Cargo.toml --features owed-fence-off \ -//! --test owed_lock -//! ``` -//! -//! drops the fence from both sides and this file must red. - -#![cfg(feature = "loom")] - -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; - -use kernel_loom::sync::OwedLock; -use loom::sync::Arc; - -/// The CPU the model turns away. -const TURNED: u32 = 3; - -/// What the releases answered in this execution; loom runs executions one at a -/// time on one OS thread, so a `std` static reset per execution is the record. -static ANSWERED: AtomicU64 = AtomicU64::new(0); - -fn record(owed: u64) { - ANSWERED.fetch_or(owed, Ordering::Relaxed); -} - -#[test] -fn a_turned_away_cpu_holds_the_lock_or_is_answered() { - static WON: AtomicBool = AtomicBool::new(false); - static TURNED_AWAY: AtomicBool = AtomicBool::new(false); - - loom::model(|| { - ANSWERED.store(0, Ordering::Relaxed); - let lock = Arc::new(OwedLock::new(0u32, record)); - let held = lock.try_lock().expect("an untaken lock is taken"); - - let other = { - let lock = lock.clone(); - loom::thread::spawn(move || lock.try_lock_or_owe(TURNED).map(drop).is_some()) - }; - drop(held); - let won = other.join().unwrap(); - - let answered = ANSWERED.load(Ordering::Relaxed) & (1 << TURNED) != 0; - assert!(won || answered, "cpu {TURNED} was turned away and nobody answered it"); - if won { - WON.store(true, Ordering::Relaxed); - } else { - TURNED_AWAY.store(true, Ordering::Relaxed); - } - }); - - assert!( - WON.load(Ordering::Relaxed) && TURNED_AWAY.load(Ordering::Relaxed), - "the model never both turned the cpu away and let it in, so one side of the handoff went unchecked", - ); -} diff --git a/kernel/pci/src/msix.rs b/kernel/pci/src/msix.rs index 4b83c066a05..94b697b9c24 100644 --- a/kernel/pci/src/msix.rs +++ b/kernel/pci/src/msix.rs @@ -124,6 +124,13 @@ impl Msix { pub fn enabled(message_control: u16) -> u16 { (message_control | ENABLE) & !FUNCTION_MASK } + + /// Message Control with every vector of the function masked at once + /// (PCIe Base §7.7.2.2), whatever each entry's own mask says; nothing + /// else changed. + pub fn function_masked(message_control: u16) -> u16 { + message_control | FUNCTION_MASK + } } #[cfg(test)] @@ -145,6 +152,14 @@ mod tests { assert_eq!(Msix::decode(ENABLE | FUNCTION_MASK, 0).unwrap().entries(), 1); } + /// Function Mask is bit 14, and masking touches neither Enable nor the + /// table size the register also carries. + #[test] + fn the_function_mask_is_bit_14_and_nothing_else() { + assert_eq!(Msix::function_masked(ENABLE | 0x07FF), ENABLE | FUNCTION_MASK | 0x07FF); + assert_eq!(Msix::function_masked(0), 1 << 14); + } + #[test] fn bir_and_offset_share_one_register() { let msix = Msix::decode(0, 0x0000_2003).unwrap(); diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index 9f50e51ac87..573c8a13db6 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -57,21 +57,6 @@ actuators! { /// 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"; - - /// Establish three nested `scheduler::Operation`s and report what each observed and restored; it stages nothing, touching no device. - sched_operation_nesting = "sched-operation-nesting"; - - /// Abandon the boot's first WRITE(10) data phase without waiting for it. - usb_transport_break = "usb-transport-break"; - - /// Sweep the boot stick from the shutdown syscall so the reset lands on a - /// controller that is moving bytes rather than on a bus idle since the - /// wedge. See `usb_gate::sweep_under_load`; judged by - /// `usb_reset_records_the_phase_it_cut`. - usb_reset_under_load = "usb-reset-under-load"; - /// Give the machine's stop no time: one sweep, and a thread not parked at /// it outlasts the stop. Judged by `machine_shutdown_short_stop`. stop_budget_spent = "stop-budget-spent"; @@ -100,9 +85,6 @@ actuators! { /// Send one NMI from the idle loop, and return from its handler via `iretq` with a second NMI already pending. nmi_nested = "nmi-nested"; - /// Run `parse_config` over nine crafted configuration descriptors at init. - xhci_descriptor_selftest = "xhci-descriptor-selftest"; - /// Walk the PCI capability list, window check and parse over crafted config-space layouts at init. pci_cap_selftest = "pci-cap-selftest"; @@ -134,12 +116,6 @@ actuators! { /// `amdvi_selftest`. amdvi_selftest = "amdvi-selftest"; - /// Remap no message, as on a platform that cannot: VT-d reads the DMAR's - /// flags with `INTR_REMAP` clear, and AMD-Vi leaves every interrupt half - /// invalid. Judged by `claim_refused_without_remapping` and - /// `amdvi_claim_refused_without_remapping`. - iommu_no_remap = "iommu-no-remap"; - /// Leave every AP holding the CR0/CR4 that INIT left it. no_ap_control_regs = "no-ap-control-regs"; @@ -214,11 +190,6 @@ actuators! { /// Have the last boot checkpoint's painter go fatal holding the panel's /// latch: a fatal path meeting a painter that will never let go. panel_painter_stalls = "panel-painter-stalls"; - - /// Leave the xHCI controller at `xhci-leave=:.` to a - /// claim: `drivers::xhci` never touches it. Read through [`xhci_left`]; - /// judged by `usbd_drives_the_spare`. - xhci_leave = "xhci-leave="; } #[cfg(feature = "boot-actuators")] @@ -243,16 +214,7 @@ static ARMED: [AtomicU64; ARM_WORDS] = [const { AtomicU64::new(0) }; ARM_WORDS]; pub fn init(cmdline: &str) { let mut armed = [0u64; ARM_WORDS]; for token in toyos_abi::boot::actuators(cmdline).filter(|t| !crate::params::claims(t)) { - // A name that ends in `=` carries the rest of its token as a value. - let name = match token.split_once('=') { - Some((name, value)) => { - let name = &token[..=name.len()]; - take_value(name, value); - name - } - None => token, - }; - arm(&mut armed, name); + arm(&mut armed, token); } for (name, implied) in IMPLIES { if is_armed(&armed, name) { @@ -270,48 +232,6 @@ pub fn init(cmdline: &str) { } } -/// The value a valued actuator carries, kept where its reader finds it. -#[cfg(feature = "boot-actuators")] -fn take_value(name: &str, value: &str) { - match name { - "xhci-leave=" => { - let at = function_address(value).unwrap_or_else(|| { - panic!("boot parameter {name}{value}: not :. in hex") - }); - XHCI_LEFT.store(u64::from(at), Ordering::Relaxed); - } - _ => panic!("boot parameter {name}{value}: this kernel declares no such valued actuator"), - } -} - -/// `bb:dd.f` in hex as a requester id, bus over device over function: a -/// machine with two controllers of one id names one by where it is. -#[cfg(feature = "boot-actuators")] -fn function_address(value: &str) -> Option { - let (bus, rest) = value.split_once(':')?; - let (dev, func) = rest.split_once('.')?; - let (bus, dev, func) = - (u8::from_str_radix(bus, 16).ok()?, u8::from_str_radix(dev, 16).ok()?, u8::from_str_radix(func, 16).ok()?); - (dev < 32 && func < 8).then_some(u16::from(bus) << 8 | u16::from(dev) << 3 | u16::from(func)) -} - -/// The requester id of the controller `xhci-leave=` names, read only once -/// that actuator is armed. -#[cfg(feature = "boot-actuators")] -static XHCI_LEFT: AtomicU64 = AtomicU64::new(0); - -/// The requester id of the xHCI controller `xhci-leave=` leaves to a claim, -/// if it is armed. -#[cfg(feature = "boot-actuators")] -pub fn xhci_left() -> Option { - xhci_leave().then(|| XHCI_LEFT.load(Ordering::Relaxed) as u16) -} - -#[cfg(not(feature = "boot-actuators"))] -pub const fn xhci_left() -> Option { - None -} - #[cfg(feature = "boot-actuators")] fn arm(armed: &mut [u64; ARM_WORDS], name: &str) { let (word, bit) = at(index_of(name)); diff --git a/kernel/src/arch/aarch64/irqchip/its.rs b/kernel/src/arch/aarch64/irqchip/its.rs index a362684b3e6..db18f02443c 100644 --- a/kernel/src/arch/aarch64/irqchip/its.rs +++ b/kernel/src/arch/aarch64/irqchip/its.rs @@ -395,8 +395,6 @@ pub struct Msi { /// Why a source could not be given a message. #[derive(Clone, Copy)] pub enum Refused { - /// It is a driver in this kernel's. - KernelDriver, /// The SMMUv3 translates it to no DeviceID at this ITS. NoDeviceId, /// Its DeviceID is past the device table's. @@ -421,11 +419,6 @@ impl Refused { impl core::fmt::Display for Refused { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Self::KernelDriver => write!( - f, - "a driver in this kernel is given no LPI: the ITS maps only a claimed function's \ - DeviceID, and such a driver has no address space for its DMA" - ), Self::NoDeviceId => write!( f, "the IORT gives it no DeviceID at this kernel's ITS behind the SMMUv3, so no \ @@ -448,13 +441,6 @@ impl core::fmt::Display for Refused { } } -/// A driver in this kernel is given no LPI: only a claimed function is -/// mapped, and a driver in this kernel on this machine has no address space -/// to do DMA in (`super::super::iommu_unit`). -pub fn msi(_source: StreamId, _irq: crate::arch::DriverIrq) -> Result { - Err(Refused::KernelDriver) -} - /// The DeviceID `routed` names at this ITS: what the SMMUv3's own routes /// map the function's stream on to, so a function the unit would not put on /// a domain is refused here, before its claim reaches `attach`. diff --git a/kernel/src/arch/aarch64/mod.rs b/kernel/src/arch/aarch64/mod.rs index 46b013f205d..5d78ea7d205 100644 --- a/kernel/src/arch/aarch64/mod.rs +++ b/kernel/src/arch/aarch64/mod.rs @@ -40,7 +40,6 @@ pub mod watchdog; pub use irqchip::its as message_unit; pub use smmu as iommu_unit; -pub use trap::DriverIrq; /// The machine every program image this kernel loads must be built for. pub const ELF_MACHINE: toyos_elf::Machine = toyos_elf::Machine::Aarch64; diff --git a/kernel/src/arch/aarch64/smmu/mod.rs b/kernel/src/arch/aarch64/smmu/mod.rs index 5a783a99758..d4e6c89a37d 100644 --- a/kernel/src/arch/aarch64/smmu/mod.rs +++ b/kernel/src/arch/aarch64/smmu/mod.rs @@ -255,7 +255,7 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: Option<&[toyos_abi:: }; crate::iommu::fault::describe(devices); - let segment = u32::from(crate::pcidev::segment()); + let segment = u32::from(toyos_acpi::SEGMENT_GROUP); let mut routes = Vec::new(); for device in devices { let function = StreamId::pci(device.bus, device.dev, device.func); diff --git a/kernel/src/arch/aarch64/trap.rs b/kernel/src/arch/aarch64/trap.rs index 5ed67d76ce5..db75aa396d3 100644 --- a/kernel/src/arch/aarch64/trap.rs +++ b/kernel/src/arch/aarch64/trap.rs @@ -528,21 +528,6 @@ pub fn install() { } } -/// What a driver in this kernel names its function's interrupt by, which -/// it is refused a message for (`irqchip::its::msi`). -#[derive(Clone, Copy, Debug)] -pub enum DriverIrq { - Xhci, -} - -impl core::fmt::Display for DriverIrq { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{self:?}'s") - } -} - -pub const XHCI_VECTOR: DriverIrq = DriverIrq::Xhci; - /// The crash report for a panic, from the frame pointer the panic handler /// stood on: the backtrace, which CPU is on which stack, and what the /// running thread was doing. diff --git a/kernel/src/arch/x86_64/amdvi/interrupt.rs b/kernel/src/arch/x86_64/amdvi/interrupt.rs index b8954f68677..46657eabb61 100644 --- a/kernel/src/arch/x86_64/amdvi/interrupt.rs +++ b/kernel/src/arch/x86_64/amdvi/interrupt.rs @@ -135,17 +135,6 @@ fn arm(source: StreamId, vector: u8) { report(source, vector, read); } -/// The message a kernel driver's `source` raises `vector` with: its own -/// compatibility message, an entry for it written first where every unit -/// remaps. -pub(in crate::arch::x86_64) fn msi(source: StreamId, vector: u8) -> Result { - if is_armed() { - arm(source, vector); - } - let (address, data) = msi_message(vector); - Ok(Msi { address, data }) -} - /// A claim's entry at `vector` for `source`, whose entry from now on passes /// nothing firmware declared for it and gives it no `SysMgt`. pub(in crate::arch::x86_64) fn claim(vector: u8, source: StreamId) -> Result { @@ -158,7 +147,11 @@ pub(in crate::arch::x86_64) fn claim(vector: u8, source: StreamId) -> Result) { } /// Every unit's SMI filter on, once every function this kernel drives is out -/// of firmware's hands: until then, a controller firmware still owns may -/// raise SMIs to be handed over (xHCI §4.22.1). A refused SMI is logged as an -/// `IO_PAGE_FAULT` (§2.1.5.1) for [`fault`]. +/// of firmware's hands. A refused SMI is logged as an `IO_PAGE_FAULT` +/// (§2.1.5.1) for [`fault`]. pub fn settle() { let mut units = UNITS.lock(); let mut everywhere = !units.is_empty(); diff --git a/kernel/src/arch/x86_64/idt/mod.rs b/kernel/src/arch/x86_64/idt/mod.rs index 35c682a19a4..c04a42b7d42 100644 --- a/kernel/src/arch/x86_64/idt/mod.rs +++ b/kernel/src/arch/x86_64/idt/mod.rs @@ -9,7 +9,6 @@ mod timer; mod tlb; pub(crate) mod unclaimed; pub(crate) mod user_dev; -mod xhci; pub(crate) use user_dev::ClaimVector; @@ -37,26 +36,6 @@ pub const ISA_VECTORS: [u8; crate::isa::MAX_ROWS] = [Vector::Isa0 as u8, Vector: /// The vector an IOMMU writes into its own `FEDATA`. pub const DMA_FAULT_VECTOR: u8 = Vector::DmaFault as u8; -/// The vector a driver in this kernel arms its function's message with: one -/// of the two below, each its own gate. -#[derive(Clone, Copy)] -pub struct DriverIrq(Vector); - -impl DriverIrq { - pub(crate) const fn vector(self) -> u8 { - self.0 as u8 - } -} - -impl core::fmt::Display for DriverIrq { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "vector {:#x}", self.vector()) - } -} - -/// The vector the xHCI controller's message-signalled interrupt carries. -pub const XHCI_VECTOR: DriverIrq = DriverIrq(Vector::Xhci); - const PF_PRESENT: u64 = 1 << 0; const PF_WRITE: u64 = 1 << 1; const PF_INSTRUCTION_FETCH: u64 = 1 << 4; @@ -262,7 +241,6 @@ idt_vectors! { // ist 2: SYSCALL's brief CPL0-on-user-stack window would fault SMAP without it. Nmi = 0x02, nmi::nmi_entry, ist 2; Timer = 0x20, timer::timer_entry; - Xhci = 0x21, xhci::xhci_entry; DmaFault = 0x25, dma_fault::dma_fault_entry; Isa0 = 0x2C, isa::isa0_entry; Isa1 = 0x2D, isa::isa1_entry; diff --git a/kernel/src/arch/x86_64/idt/xhci.rs b/kernel/src/arch/x86_64/idt/xhci.rs deleted file mode 100644 index 6ecce39df01..00000000000 --- a/kernel/src/arch/x86_64/idt/xhci.rs +++ /dev/null @@ -1,17 +0,0 @@ -use super::device_irq::device_irq_entry; -use crate::irq_ring::IrqSource; - -// Lock-free and heap-free — the event ring is polled by drivers::xhci::poll_if_pending, never here. -extern "sysv64" fn xhci_handler() { - crate::arch::percpu::irq_took!(Xhci); - let timestamp = crate::clock::nanos_since_boot(); - crate::irq_ring::isr_publish(IrqSource::Xhci, timestamp); - // Forces a scheduler entry on IRQ return so drain_irqs polls now, not at the next quantum tick. - crate::preempt::set_need_resched(); - crate::arch::apic::eoi(); -} - -device_irq_entry! { - /// xHCI MSI-X entry (see `device_irq_entry` for the asm contract). - pub(super) fn xhci_entry => xhci_handler -} diff --git a/kernel/src/arch/x86_64/iommu_unit.rs b/kernel/src/arch/x86_64/iommu_unit.rs index 9114a2d7cc0..b34cbc47ba7 100644 --- a/kernel/src/arch/x86_64/iommu_unit.rs +++ b/kernel/src/arch/x86_64/iommu_unit.rs @@ -229,7 +229,7 @@ pub mod fault { /// with, and the claim's vector it delivers. pub mod interrupt { use super::{backend, Backend}; - use crate::arch::x86_64::idt::{user_dev, ClaimVector, DriverIrq}; + use crate::arch::x86_64::idt::{user_dev, ClaimVector}; use crate::arch::x86_64::{amdvi, vtd}; use crate::iommu::StreamId; @@ -321,14 +321,6 @@ pub mod interrupt { } } - /// The message a kernel driver's `source` raises `irq` with. - pub fn msi(source: StreamId, irq: DriverIrq) -> Result { - match backend() { - Backend::AmdVi => amdvi::interrupt::msi(source, irq.vector()), - Backend::Vtd | Backend::None => vtd::interrupt::msi(source, irq), - } - } - /// A vector for `pcidev` function `function` and an entry delivering it, /// written for `source` and published to every unit before this returns. pub fn claim(function: usize, source: StreamId) -> Result { diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs index fe3ce2adc0c..15ce32e035f 100644 --- a/kernel/src/arch/x86_64/mod.rs +++ b/kernel/src/arch/x86_64/mod.rs @@ -49,7 +49,6 @@ pub use idt as trap; pub use iommu_unit::interrupt as message_unit; pub use apic::MSI_DOORBELL; -pub use idt::DriverIrq; /// The machine every program image this kernel loads must be built for. pub const ELF_MACHINE: toyos_elf::Machine = toyos_elf::Machine::X86_64; diff --git a/kernel/src/arch/x86_64/vtd/interrupt.rs b/kernel/src/arch/x86_64/vtd/interrupt.rs index cfc2f03b280..b0706752daf 100644 --- a/kernel/src/arch/x86_64/vtd/interrupt.rs +++ b/kernel/src/arch/x86_64/vtd/interrupt.rs @@ -38,7 +38,6 @@ use alloc::vec::Vec; use kernel::owners::Owners; use super::super::apic::{narrow_destination, MSG_DEST}; -use super::super::idt::DriverIrq; use crate::iommu::StreamId; use crate::sync::Lock; @@ -213,17 +212,6 @@ pub(in crate::arch::x86_64) fn message(index: usize) -> Msi { } } -/// The message a kernel driver's `source` raises `irq` with: an entry of its -/// own where the table is armed, and the compatibility format where no unit -/// remaps, which is what that message has always said. -pub fn msi(source: StreamId, irq: DriverIrq) -> Result { - if !is_armed() { - let (address, data) = super::super::apic::msi_message(irq.vector()); - return Ok(Msi { address, data }); - } - Ok(message(allocate(source, irq.vector(), MSG_DEST, false)?)) -} - /// An entry delivering a claim's `vector`, written for `source` and published /// to every unit before this returns; its index, held until [`release`]. pub(in crate::arch::x86_64) fn claim(vector: u8, source: StreamId) -> Result { diff --git a/kernel/src/arch/x86_64/vtd/mod.rs b/kernel/src/arch/x86_64/vtd/mod.rs index a6bbb126c8c..8acbe253d5d 100644 --- a/kernel/src/arch/x86_64/vtd/mod.rs +++ b/kernel/src/arch/x86_64/vtd/mod.rs @@ -251,10 +251,6 @@ pub fn init(rsdp_addr: u64, devices: &[PciDevice], windows: Option<&[RootBridgeW /// interrupt to whatever the handle bits spell. Every condition below therefore /// refuses for the machine, not for the unit that failed it. fn remappable(ready: &[(Unit, Plan)], described: usize, flags: u8) -> Option { - // Stands in for firmware, so the flag's own arm is what refuses. - #[cfg(feature = "boot-actuators")] - let flags = - if crate::actuator::iommu_no_remap() { flags & !dmar::FLAG_INTR_REMAP } else { flags }; if ready.is_empty() { return None; } diff --git a/kernel/src/blackbox.rs b/kernel/src/blackbox.rs index 6f3913e06a1..f37b1955ec4 100644 --- a/kernel/src/blackbox.rs +++ b/kernel/src/blackbox.rs @@ -99,7 +99,6 @@ pub fn record_panic(records: &[u8]) { // The machine's census, which a death takes as a stop does: atomics // alone, so it is inside this region's rules (`crate::census`). let _ = core::fmt::Write::write_fmt(&mut report, format_args!("{}", crate::census::Sealed)); - crate::log::recovery::seal_into(&mut report); report.tail(records, toyos_blackbox::RECORD_OPENS_WITH); report.seal(State::Panic, stamp, identity); }); @@ -134,13 +133,6 @@ pub fn append(account: impl FnOnce(&mut dyn core::fmt::Write)) -> bool { reopened(|lines| account(lines)) } -/// Append the recovery section to what this boot already sealed: a boot that -/// hands the machine back says what its transport went through as a boot that -/// dies does, because the stick that would carry the log may be what broke. -pub fn append_recovery() -> bool { - reopened(|lines| lines.block(crate::log::recovery::seal_into)) -} - fn reopened(account: impl FnOnce(&mut toyos_blackbox::Account<'_, &mut dyn FnMut()>)) -> bool { let mut appended = false; // **The envelope is the one already on the page**, state, stamp and @@ -183,10 +175,6 @@ pub fn record_wedge(said: core::fmt::Arguments, records: &[u8]) { // Why first, then as much of the tail as is left: a report cut to its // tail alone does not say what ended the machine. let _ = core::fmt::Write::write_fmt(&mut report, said); - // Above the tail and not in it: on a boot that outlived its transport's - // break the tail is everything written since, and the cut takes the - // break first. - crate::log::recovery::seal_into(&mut report); report.tail(records, toyos_blackbox::RECORD_OPENS_WITH); report.seal(State::Wedged, stamp, identity); }); diff --git a/kernel/src/block.rs b/kernel/src/block.rs deleted file mode 100644 index 1773e72539f..00000000000 --- a/kernel/src/block.rs +++ /dev/null @@ -1,504 +0,0 @@ -//! One device object per physical device; a consumer takes a [`Handle`] and a -//! [`Partition`] over it and can name no block outside its span. -//! -//! **A block has one holder.** Every view is made by [`Partition::of`], which -//! refuses a span any live view holds a block of, and the view's hold goes -//! with the last clone of it (`toyos_blockhold` decides both). The kernel's mounts hold their partitions this -//! way ([`Holder::Kernel`]), and so does a process's partition claim -//! ([`Holder::Claim`]): a mounted partition cannot be claimed, a claimed one -//! cannot be mounted or claimed twice, and no kernel cache ever holds a block -//! a claim writes — so a claim's transfers need no invalidation and read the -//! device. -//! -//! **A flush answers for its writer's own writes.** A device's flush is the -//! whole device's, but every write and flush goes through a [`Locked`] device -//! that knows whose it is, and `toyos_blockhold` keeps each writer's account -//! against the disk's loss count ([`BlockDevice::losses`]): a flush fails for -//! exactly the writers whose writes were reported before a loss — at each -//! one's own next flush, once, whoever flushes first — and a loss a released -//! span owed is told to the next holder of its blocks. -//! -//! Lock order: a consumer's own lock, then [`Handle::lock`]; never the reverse, -//! and never two devices at once. [`DEVICES`] is a leaf taken alone, and a -//! device's holds are taken last, alone or under its device's lock. - -use alloc::boxed::Box; -use alloc::collections::BTreeMap; -use alloc::sync::Arc; - -use crate::mm::PAGE_SIZE; -use crate::scheduler::Operation; -use crate::sync::{Lock, LockGuard}; -use crate::time::{Budget, Cadence, Deadline, Duration}; -use toyos_blockhold::{Holds, Lost, Writer}; - -/// Unique identifier for a block device; [`register`] issues each at most once. -pub type DeviceId = u32; - -/// How long one operation on a block device may spend inside the device before it is refused. -// Bounds the longest stretch held pinned with preemption off; raising it lengthens audio-path stalls directly. -pub const OPERATION: Budget = Budget::of( - Duration::from_secs(2), - "the block-device operation is refused as one that would block, and the \ - caller's own give-up policy decides whether to ask again", -); - -/// Total time a sequence of block-device operations may spend before the volume is declared failed. -pub const DEADMAN: Budget = Budget::of( - Duration::from_secs(120), - "the run of retries ends, the volume is declared failed, and the caller is \ - told with a device error rather than another ask-again", -); - -/// How soon the retry loop may ask again after its first refused attempt. -pub const RETRY_SOONEST: Cadence = Cadence::every( - Duration::from_millis(10), - "one quantum parked between attempts; the refusal itself issued nothing", -); - -/// The ceiling the retry interval doubles up to. -pub const RETRY_SLOWEST: Cadence = Cadence::every( - OPERATION.duration(), - "between two pinned attempts the machine gets at least as long as one \ - attempt may pin", -); - -/// Backoff for attempt `attempt` (>= 2): doubles from `RETRY_SOONEST` to `RETRY_SLOWEST`. -pub(crate) fn backoff_step(attempt: u32) -> Duration { - Duration::from_nanos( - RETRY_SOONEST - .nanos() - .saturating_mul(1u64 << (attempt - 2).min(32)) - .min(RETRY_SLOWEST.nanos()), - ) -} - -/// Parks the calling task between two refused block-operation attempts; the first call only yields. -/// Must not be called by a caller already registered on a watch — a second registration panics. -pub(crate) fn between_attempts(attempt: u32) { - if attempt <= 1 { - crate::scheduler::yield_now(); - return; - } - let parkable = crate::scheduler::Parkable::at_entry(); - let Some(handle) = crate::sched::driver::current_handle() else { - return; - }; - let deadline = Deadline::at(crate::clock::now() + backoff_step(attempt)); - let _ = crate::watch::wait_until( - &parkable, - handle.watch(), - 0, - kernel::sched::task::WaitClass::Other, - deadline, - || false, - ); -} - -/// Operations open right now on a thread the machine's stop stops, and how -/// many such operations this boot began. -static OPEN_OPERATIONS: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); -static BEGUN_OPERATIONS: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); - -/// Open on a thread the stop stops, and begun on this boot. -pub fn userland_operations() -> (u32, u64) { - ( - OPEN_OPERATIONS.load(core::sync::atomic::Ordering::Relaxed), - BEGUN_OPERATIONS.load(core::sync::atomic::Ordering::Relaxed), - ) -} - -/// Whether the stop this count is read for stops the thread opening an -/// operation now: it stops every userland thread and no kernel thread. -fn counted() -> bool { - !crate::sched::kthread::current_is_kernel_thread() && crate::arch::percpu::current_pid().is_some() -} - -#[must_use = "the operation lasts exactly as long as this guard"] -pub struct OpenOperation { - _deadline: Operation, - /// Decided at the open and not at the close, so the two ends of one - /// operation cannot disagree about whether it was counted. - counted: bool, -} - -impl Drop for OpenOperation { - fn drop(&mut self) { - if self.counted { - OPEN_OPERATIONS.fetch_sub(1, core::sync::atomic::Ordering::Relaxed); - } - } -} - -/// Declares the running context inside one block-device operation, bounded by `OPERATION`, until the guard drops. -// An absolute deadline, not a relative duration: it crosses into a driver that loops, and re-basing per command would bound each command instead of the whole operation. -#[must_use = "the operation lasts exactly as long as this guard"] -pub fn begin_operation() -> OpenOperation { - let counted = counted(); - if counted { - OPEN_OPERATIONS.fetch_add(1, core::sync::atomic::Ordering::Relaxed); - BEGUN_OPERATIONS.fetch_add(1, core::sync::atomic::Ordering::Relaxed); - } - OpenOperation { - _deadline: Operation::begin(Deadline::at( - crate::clock::now() + OPERATION.duration(), - )), - counted, - } -} - -/// Why an operation on this trait did not complete. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum BlockError { - /// The device itself failed the operation. - Device, - /// Refused before it was attempted because the operation's time budget expired; safe to retry. - BudgetExpired, -} - -/// The one conversion between the kernel's block verdict and the bcachefs -/// crate's: a `BudgetExpired` was never attempted and stays retryable. -impl bcachefs::TransferError for BlockError { - fn refused_before_attempt(&self) -> bool { - matches!(self, BlockError::BudgetExpired) - } -} - -pub type BlockResult = Result<(), BlockError>; - -/// Block-oriented storage device interface; all I/O is in whole 4KB blocks. -pub trait BlockDevice: Send { - fn device_id(&self) -> DeviceId; - fn block_count(&self) -> u64; - - /// Reads `count` contiguous blocks starting at `lba` into `buf` (`buf.len()` must equal `count as usize * 4096`). - #[must_use = "a failed read leaves the buffer holding whatever it held before"] - fn read_blocks(&mut self, lba: u64, count: u32, buf: &mut [u8]) -> BlockResult; - - /// Writes `count` contiguous blocks starting at `lba` from `buf` (`buf.len()` must equal `count as usize * 4096`). - #[must_use = "a failed write did not reach the device"] - fn write_blocks(&mut self, lba: u64, count: u32, buf: &[u8]) -> BlockResult; - - /// Flush any hardware write caches to persistent storage. - #[must_use = "a failed flush means the writes before it are not durable"] - fn flush(&mut self) -> BlockResult; - - /// How many times this disk has lost writes it reported complete — a - /// device that left owing a flush and was taken back - /// (`toyos_xhci::flush`) — as the device that ran the last operation to - /// return counts it. Never decreases; a disk that is never taken back - /// answers 0. - fn losses(&self) -> u64; -} - -struct Device { - id: DeviceId, - blocks: u64, - dev: Lock>, - holds: Lock>, -} - -/// A shared handle; there is never a second object for one [`DeviceId`]. -#[derive(Clone)] -pub struct Handle(Arc); - -static DEVICES: Lock> = Lock::new(BTreeMap::new()); - -/// Registers `dev` under the id it answers to, or refuses when that id is taken: -/// two devices sharing a number would serve each other's blocks out of one -/// cache, which a plain insert here would arrange in silence. -#[must_use = "a refused registration leaves the device unreachable"] -pub fn register(dev: Box) -> Option { - let id = dev.device_id(); - let blocks = dev.block_count(); - let mut devices = DEVICES.lock(); - if let Some(held) = devices.get(&id) { - log!( - "block: device {id} is already registered with {} blocks; refusing a second device \ - claiming that number ({blocks} blocks) — one cache keys its pages on it", - held.0.blocks - ); - return None; - } - let holds = Holds::new(); - let handle = - Handle(Arc::new(Device { id, blocks, dev: Lock::new(dev), holds: Lock::new(holds) })); - devices.insert(id, handle.clone()); - log!("block: device {id} registered, {blocks} blocks"); - Some(handle) -} - -pub fn open(id: DeviceId) -> Option { - DEVICES.lock().get(&id).cloned() -} - -pub fn registered() -> alloc::vec::Vec { - DEVICES.lock().values().cloned().collect() -} - -impl Handle { - pub fn device_id(&self) -> DeviceId { - self.0.id - } - - /// What the driver reported at registration; never re-asked, so a view's bound cannot move under it. - pub fn block_count(&self) -> u64 { - self.0.blocks - } - - /// The device itself, with its queue serialised for as long as the guard - /// lives, for a writer that holds no view of it. - pub fn lock(&self) -> Locked<'_> { - Locked { dev: self.0.dev.lock(), device: &self.0, writer: Writer::Unspanned } - } - - /// Who holds exactly blocks `first..end` of this device, from the holds - /// every view takes: the record [`Partition::of`] refuses against, so no - /// copy of it can disagree. - pub fn holder(&self, first: u64, end: u64) -> Option { - self.0.holds.lock().holder_of(first, end) - } - - /// Whether writes this device's disk lost by the count `losses` are - /// reported to no writer yet: what a flush from below the block layer asks - /// before it calls the disk flushed. - pub fn untold(&self, losses: u64) -> bool { - self.0.holds.lock().untold(losses) - } -} - -/// A device with its queue serialised, whose writes and flushes are one -/// writer's: a flush through it fails if a write of that writer's was reported -/// before the disk lost it, and for no other writer's. -pub struct Locked<'a> { - dev: LockGuard<'a, Box>, - device: &'a Device, - writer: Writer, -} - -impl BlockDevice for Locked<'_> { - fn device_id(&self) -> DeviceId { - self.dev.device_id() - } - - fn block_count(&self) -> u64 { - self.dev.block_count() - } - - fn read_blocks(&mut self, lba: u64, count: u32, buf: &mut [u8]) -> BlockResult { - self.dev.read_blocks(lba, count, buf) - } - - fn write_blocks(&mut self, lba: u64, count: u32, buf: &[u8]) -> BlockResult { - let done = self.dev.write_blocks(lba, count, buf); - if done.is_ok() { - let losses = self.dev.losses(); - self.device.holds.lock().wrote(self.writer, losses); - } - done - } - - fn flush(&mut self) -> BlockResult { - self.dev.flush()?; - let losses = self.dev.losses(); - let Err(Lost { holder }) = self.device.holds.lock().flushed(self.writer, losses) else { - return Ok(()); - }; - let whose = match holder { - Some(holder) => alloc::format!("{holder}"), - None => alloc::string::String::from("a writer holding no view"), - }; - log!( - "block: device {}: writes {whose} made before its disk came back owing a flush \ - may not have survived, and its flush says so", - self.device.id - ); - Err(BlockError::Device) - } - - fn losses(&self) -> u64 { - self.dev.losses() - } -} - -/// Who holds a span of a device. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Holder { - /// Something in this kernel — a mount, a probe — named for the log. - Kernel(&'static str), - /// A process, through its claim of the partition this unique GUID names. - Claim(toyos_gpt::Guid), -} - -impl core::fmt::Display for Holder { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Kernel(what) => write!(f, "the kernel ({what})"), - Self::Claim(guid) => write!(f, "a process's claim of partition {guid}"), - } - } -} - -/// A span's hold, shared by every clone of the view that took it: a transfer -/// in flight on a clone keeps the span held after the view it was cloned from -/// has gone. -struct Hold { - device: Arc, - first: u64, -} - -impl Drop for Hold { - fn drop(&mut self) { - self.device.holds.lock().release(self.first); - } -} - -/// Why [`Partition::of`] made no view. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ViewRefused { - /// The span is empty or runs off the device. - OffDevice, - /// A block of it is already held. - Held(Holder), -} - -/// Why [`span_blocks`] found no whole-block span. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum SpanRefused { - /// The LBA range does not fit a byte offset or length in `u64`. - Overflow, - /// The byte range does not begin or end on a [`PAGE_SIZE`] boundary, - /// carried along so a caller that logs can name what it computed. - NotWhole { start_bytes: u64, len_bytes: u64 }, -} - -/// A partition's `(start_lba, lba_count)`, in its device's `lba_bytes`-byte -/// logical blocks, converted to the [`PAGE_SIZE`] blocks every [`Partition`] -/// view is made in. The one spelling of that conversion every caller that -/// makes or reports a view agrees with bit-for-bit — a second copy that -/// drifts from this one would let a held span read as free, or the reverse. -pub fn span_blocks(start_lba: u64, lba_count: u64, lba_bytes: u32) -> Result<(u64, u64), SpanRefused> { - let lba = u64::from(lba_bytes); - let (Some(start), Some(len)) = (start_lba.checked_mul(lba), lba_count.checked_mul(lba)) else { - return Err(SpanRefused::Overflow); - }; - if start % PAGE_SIZE != 0 || len % PAGE_SIZE != 0 { - return Err(SpanRefused::NotWhole { start_bytes: start, len_bytes: len }); - } - Ok((start / PAGE_SIZE, len / PAGE_SIZE)) -} - -/// One consumer's view of one span of a device, in whole [`BlockDevice`] blocks: -/// a read at `block_count()` is refused by name, never served from past its end. -#[derive(Clone)] -pub struct Partition { - handle: Handle, - first_block: u64, - blocks: u64, - _hold: Arc, -} - -impl Partition { - /// `blocks` blocks from `first_block`, held for `holder` until the last - /// clone of the view drops — or refused when the span is empty, off the - /// device, or overlaps a span another view holds. - pub fn of( - handle: Handle, - first_block: u64, - blocks: u64, - holder: Holder, - ) -> Result { - let end = first_block - .checked_add(blocks) - .filter(|&end| blocks != 0 && end <= handle.block_count()) - .ok_or(ViewRefused::OffDevice)?; - handle.0.holds.lock().hold(first_block, end, holder).map_err(ViewRefused::Held)?; - let hold = Arc::new(Hold { device: handle.0.clone(), first: first_block }); - Ok(Self { handle, first_block, blocks, _hold: hold }) - } - - pub fn device_id(&self) -> DeviceId { - self.handle.device_id() - } - - pub fn block_count(&self) -> u64 { - self.blocks - } - - /// Whether `count` blocks from `block` end inside the view — asked of a - /// caller's numbers, so a refusal is the caller's answer and not a line in - /// the kernel's log. - pub fn fits(&self, block: u64, count: u32) -> bool { - block.checked_add(count as u64).is_some_and(|end| end <= self.blocks) - } - - /// The device, serialised, with every write and flush through it this - /// view's own. - pub fn lock(&self) -> Locked<'_> { - let device = &*self.handle.0; - Locked { dev: device.dev.lock(), device, writer: Writer::Span(self.first_block) } - } - - /// The device block `block` names, or a refusal past the view's end. - pub fn locate(&self, block: u64, count: u32) -> Result { - if self.fits(block, count) { - Ok(self.first_block + block) - } else { - Err(self.past_end(block, count)) - } - } - - #[cold] - #[inline(never)] - fn past_end(&self, block: u64, count: u32) -> BlockError { - log!( - "block: device {} partition at +{}: refusing {count} block(s) at {block} on a view \ - of {} blocks", - self.handle.device_id(), - self.first_block, - self.blocks - ); - BlockError::Device - } - - #[must_use = "a failed read leaves the buffer holding whatever it held before"] - pub fn read_blocks(&self, block: u64, count: u32, buf: &mut [u8]) -> BlockResult { - let at = self.locate(block, count)?; - self.lock().read_blocks(at, count, buf) - } - - #[must_use = "a failed write did not reach the device"] - pub fn write_blocks(&self, block: u64, count: u32, buf: &[u8]) -> BlockResult { - let at = self.locate(block, count)?; - self.lock().write_blocks(at, count, buf) - } - - #[must_use = "a failed flush means the writes before it are not durable"] - pub fn flush(&self) -> BlockResult { - self.lock().flush() - } -} - -/// Pages the file data cache may hold. -pub fn file_cache_pages() -> usize { - let (total, _) = crate::mm::pmm::stats(); - (((total / 64) / PAGE_SIZE) as usize).clamp(2048, 65536) -} - -/// What the storage drivers count for the machine. -pub mod census { - use core::sync::atomic::{AtomicU64, Ordering}; - - /// Every command a storage driver has put to a disk, counted - /// where each driver hands one to its transport: the one number that says a - /// stretch of the boot needed no disk. - static COMMANDS: AtomicU64 = AtomicU64::new(0); - - pub fn command_issued() { - COMMANDS.fetch_add(1, Ordering::Relaxed); - } - - pub fn commands_issued() -> u64 { - COMMANDS.load(Ordering::Relaxed) - } -} diff --git a/kernel/src/cpu_local.rs b/kernel/src/cpu_local.rs index 2a8115a0df8..7bc5c79c6bc 100644 --- a/kernel/src/cpu_local.rs +++ b/kernel/src/cpu_local.rs @@ -12,12 +12,10 @@ use crate::cpu_slice::CpuSlice; pub struct CpuLocal { pub arch: crate::arch::CpuLocal, pub sched: crate::sched::driver::Local, - pub no_task_operation: crate::scheduler::OperationSlot, pub hardlockup: crate::hardlockup::Local, pub deadline: crate::deadline::Local, pub hw: crate::hw::Local, pub irq_census: crate::irq_census::Local, - pub irq_ring: crate::irq_ring::Local, pub counters: crate::counters::Local, #[cfg(feature = "mask-windows")] pub windows: crate::windows::Local, @@ -29,12 +27,10 @@ pub fn size(capacity: usize) { TABLE.size(capacity, |_| alloc::boxed::Box::leak(alloc::boxed::Box::new(CpuLocal { arch: crate::arch::CpuLocal::new(), sched: crate::sched::driver::Local::new(), - no_task_operation: crate::scheduler::OperationSlot::new(), hardlockup: crate::hardlockup::Local::new(), deadline: crate::deadline::Local::new(), hw: crate::hw::Local::new(), irq_census: crate::irq_census::Local::new(), - irq_ring: crate::irq_ring::Local::new(), counters: crate::counters::Local::new(), #[cfg(feature = "mask-windows")] windows: crate::windows::Local::new(), diff --git a/kernel/src/deadline.rs b/kernel/src/deadline.rs index 4b54e5e59b8..b5e7feddfa0 100644 --- a/kernel/src/deadline.rs +++ b/kernel/src/deadline.rs @@ -42,7 +42,6 @@ use core::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering::Relaxed}; pub const PHASES: &[&str] = &[ "before the first boot phase", "CPU ready", - "storage ready", "peripherals ready", "subsystems ready", "devices ready", diff --git a/kernel/src/device.rs b/kernel/src/device.rs index 9fb1b0f643d..0c8cbd7a3c7 100644 --- a/kernel/src/device.rs +++ b/kernel/src/device.rs @@ -2,7 +2,6 @@ use alloc::sync::Arc; use crate::object::device::{DeviceClaim, DeviceInfo}; use crate::object::shm::Region; -use crate::{keyboard, mouse}; use toyos_abi::FramebufferInfo; use crate::sync::Lock; pub use toyos_abi::syscall::DeviceType; @@ -40,9 +39,8 @@ pub struct Claim { /// What one claim holds. A class is at most one device on this machine and a /// per-class flag says whether it is taken; a PCI function is one of several, -/// so what it gives back is its `pcidev` slot; a partition's exclusivity is its -/// view's own hold on the blocks (`block::Partition::of`); an ISA function's and -/// the ACPI fixed hardware's is its `isa` row's. +/// so what it gives back is its `pcidev` slot; an ISA function's and the ACPI +/// fixed hardware's exclusivity is its `isa` row's. /// /// The slot and the row are held as what their modules' calls take, and the /// drop of each is its release: a call borrows one from the claim's `Held`, so @@ -50,7 +48,6 @@ pub struct Claim { enum Claimed { Class(DeviceType), PciFunction(crate::pcidev::Binding), - Partition(crate::block::Partition), Isa(crate::isa::Row), Acpi(crate::isa::Row), } @@ -75,7 +72,7 @@ impl Claim { pub(crate) fn binding(&self) -> Option<&crate::pcidev::Binding> { match &self.what { Claimed::PciFunction(binding) => Some(binding), - Claimed::Class(_) | Claimed::Partition(_) | Claimed::Isa(_) | Claimed::Acpi(_) => None, + Claimed::Class(_) | Claimed::Isa(_) | Claimed::Acpi(_) => None, } } @@ -84,7 +81,7 @@ impl Claim { pub(crate) fn row(&self) -> Option<&crate::isa::Row> { match &self.what { Claimed::Isa(row) | Claimed::Acpi(row) => Some(row), - Claimed::Class(_) | Claimed::PciFunction(_) | Claimed::Partition(_) => None, + Claimed::Class(_) | Claimed::PciFunction(_) => None, } } @@ -93,15 +90,7 @@ impl Claim { match &self.what { Claimed::PciFunction(binding) => crate::pcidev::add_poll(binding, entry), Claimed::Isa(row) | Claimed::Acpi(row) => crate::isa::add_poll(row, entry), - Claimed::Class(_) | Claimed::Partition(_) => unreachable!("a poll registered through a claim on no function or row"), - } - } - - /// The view a partition claim transfers through, or `None` for any other. - pub(crate) fn partition(&self) -> Option<&crate::block::Partition> { - match &self.what { - Claimed::Partition(view) => Some(view), - Claimed::Class(_) | Claimed::PciFunction(_) | Claimed::Isa(_) | Claimed::Acpi(_) => None, + Claimed::Class(_) => unreachable!("a poll registered through a claim on no function or row"), } } } @@ -114,8 +103,6 @@ impl Drop for Claim { // binding's drop owns that order, and this is where a dying // process reaches it. Claimed::PciFunction(_) => {} - // The view drops with this, and its hold with the last clone of it. - Claimed::Partition(_) => {} // The row's drop is its release. Claimed::Isa(_) => {} // Back to the mode the firmware handed over, and then the row's drop. @@ -137,9 +124,6 @@ pub enum ClaimError { Owned, /// This machine has no such device — no driver ever registered one. Absent, - /// The partition GUID is carried twice. Refused rather than resolved to - /// the first: picking would be the kernel deciding. - Ambiguous, /// A driver in this kernel already binds the device; two drivers on one /// device is not something a claim may create. KernelDriven, @@ -158,22 +142,11 @@ pub enum ClaimError { /// failure after `acquire` cannot leave a device held by nobody. /// /// `selector` says *which* device where the class alone does not — a PCI -/// function's arrival in its first word, a partition's GUID in both — and the +/// function's arrival in its first word, an ISA set's ports and lines — and the /// syscall has refused a word the class does not read. pub fn try_claim(class: DeviceType, selector: [u64; 2]) -> Result, ClaimError> { // Availability is checked before acquiring, so an absent device reports `Absent`, not `Owned`. match class { - DeviceType::Keyboard => { - 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 => { - let claim = Claim::acquire(class)?; - mouse::discard_queued(); - Ok(DeviceClaim::new(class, DeviceInfo::Events, claim)) - } DeviceType::Framebuffer => { let screen = (*FB_INFO.lock()).clone().ok_or(ClaimError::Absent)?; let claim = Claim::acquire(class)?; @@ -189,16 +162,6 @@ pub fn try_claim(class: DeviceType, selector: [u64; 2]) -> Result { - let found = crate::gpt::claimable(toyos_abi::part::PartGuid::from_wire(selector))?; - let view = partition_view(&found)?; - let info = toyos_abi::part::PartitionInfo { - blocks: view.block_count(), - unique_guid: found.unique.0, - }; - let claim = Claim { what: Claimed::Partition(view) }; - Ok(DeviceClaim::new(class, DeviceInfo::Partition(info), claim)) - } DeviceType::Isa => { let set = toyos_abi::syscall::IsaId::from_wire(selector).ok_or(ClaimError::Absent)?; // The row's own guard, taken inside as a PCI function's is. @@ -215,54 +178,6 @@ pub fn try_claim(class: DeviceType, selector: [u64; 2]) -> Result Result { - use crate::block::{span_blocks, Holder, SpanRefused, ViewRefused}; - let volume = found.volume; - let guid = found.unique; - let handle = crate::block::open(volume.device).ok_or(ClaimError::Absent)?; - let (first_block, blocks) = - match span_blocks(volume.start_lba, volume.blocks, volume.lba_bytes) { - Ok(span) => span, - Err(SpanRefused::Overflow) => { - log!("partclaim: {guid} claims LBA {}+{} of {} bytes, which is no byte range", - volume.start_lba, volume.blocks, volume.lba_bytes); - return Err(ClaimError::Unusable); - } - Err(SpanRefused::NotWhole { start_bytes, len_bytes }) => { - log!( - "partclaim: {guid} is at {start_bytes}+{len_bytes} bytes on device {}, which \ - is not whole {}-byte blocks — a transfer would share one with its neighbour", - volume.device, - crate::mm::PAGE_SIZE - ); - return Err(ClaimError::Unusable); - } - }; - match crate::block::Partition::of(handle, first_block, blocks, Holder::Claim(guid)) { - Ok(view) => Ok(view), - Err(ViewRefused::Held(Holder::Kernel(what))) => { - log!("partclaim: {guid} is held by the kernel ({what}) and cannot be claimed"); - Err(ClaimError::KernelDriven) - } - Err(ViewRefused::Held(Holder::Claim(_))) => Err(ClaimError::Owned), - Err(ViewRefused::OffDevice) => { - log!( - "partclaim: {guid} is at {first_block}+{blocks} blocks, off device {}", - volume.device - ); - Err(ClaimError::Unusable) - } - } -} - fn shm(region: Region) -> Arc { crate::object::shm::SharedMemObject::over(region) } diff --git a/kernel/src/drivers/mod.rs b/kernel/src/drivers/mod.rs index b7d0f5036cb..b2774fed09c 100644 --- a/kernel/src/drivers/mod.rs +++ b/kernel/src/drivers/mod.rs @@ -8,8 +8,6 @@ pub mod serial; pub mod serial_lock; pub mod acpi; pub mod pci; -pub mod xhci; -pub mod usb_storage; pub mod virtio; pub mod virtio_console; pub mod gop; diff --git a/kernel/src/drivers/pci.rs b/kernel/src/drivers/pci.rs index d38dd8393d7..fd3bc681a00 100644 --- a/kernel/src/drivers/pci.rs +++ b/kernel/src/drivers/pci.rs @@ -101,9 +101,7 @@ pub fn stop_bus_mastering(config: Mmio) { /// One function's ECAM window: PCIe extended config space, PCI 3.0 §7.2.2. /// -/// Declared once because two readers deal in it — an ECAM window carves it and -/// the reset-time xHCI stop rebuilds one from a bare address. -pub const CONFIG_BYTES: u64 = 4096; +const CONFIG_BYTES: u64 = 4096; /// A PCI function, by its bus, device and function and the configuration /// space its ECAM window gives it. @@ -269,23 +267,6 @@ impl PciDevice { self.mmio.read_u16(COMMAND); } - /// Point this function's [`MSIX_ENTRY`] at `irq` and enable it: a kernel - /// driver's arming, whose message the architecture makes - /// ([`crate::iommu::driver_msi`]). A claimed function is armed by - /// [`arm_claimed_msix`] instead. - /// - /// Answers the entry's own window, which stays this kernel's: masking is a - /// write to it, and a claimant that could reach it could aim the device's - /// message at any address the LAPIC decodes. - /// - /// `None` where it has no MSI-X entry this kernel can write, or it is - /// refused a message. - pub(in crate::drivers) fn enable_msix(&self, irq: crate::arch::DriverIrq) -> Option { - let entry = self.msix_entry().ok()?; - let (address, data) = self.message(irq)?; - Some(self.write_msix(entry, address, data)) - } - pub(in crate::drivers) fn msix_entry(&self) -> Result, NoEntry> { let cap = self.capability(msix::CAP_ID).map_err(NoEntry::NoTable)?; let control = cap.read_u16(msix::MESSAGE_CONTROL); @@ -340,42 +321,9 @@ impl PciDevice { self.bus, self.dev, self.func); } - /// The address and data this function's interrupt registers take for `irq`. - /// - /// `None` refuses the device: the message it would otherwise write is one - /// nothing delivers. - fn message(&self, irq: crate::arch::DriverIrq) -> Option<(u32, u32)> { - match crate::iommu::driver_msi(self.bus, self.dev, self.func, irq) { - Ok(message) => Some((message.address, message.data)), - Err(why) => { - log!("PCI {:02x}:{:02x}.{}: not armed — {why}", self.bus, self.dev, self.func); - None - } - } - } - - /// Point this function's single MSI message at `irq` and enable it: a - /// kernel driver's arming, as [`Self::enable_msix`] is. - /// - /// **A driver in this kernel may arm this however the MSI-X walk failed**, a - /// list that ended early included: it hands no BAR of its function to a - /// holder, so an MSI-X table past that link is one nobody but this kernel - /// could reach. A hand-over is the caller that has to tell the two apart, - /// and `crate::pcidev`'s header says why. - pub(in crate::drivers) fn enable_msi(&self, irq: crate::arch::DriverIrq) -> bool { - let Ok(cap) = self.capability(msi::CAP_ID) else { - return false; - }; - let Some((address, data)) = self.message(irq) else { - return false; - }; - self.write_msi(cap, address, data); - true - } - /// Point an IOMMU unit's own function's MSI at `address`/`data` and /// enable it: the one message no unit remaps (AMD 48882 Capability Offset - /// 10h), so it is armed here rather than through [`Self::message`]. + /// 10h), so it is armed here and not through an interrupt remapping entry. /// `false` where the function has no MSI capability. pub(crate) fn arm_unit_msi(&self, address: u32, data: u32) -> bool { let Ok(cap) = self.capability(msi::CAP_ID) else { @@ -404,6 +352,14 @@ impl PciDevice { ); } + /// Mask every MSI-X vector at once with the capability's Function Mask: + /// nothing where the function has no MSI-X capability. + pub fn mask_msix_function(&self) { + let Ok(cap) = self.capability(msix::CAP_ID) else { return }; + let control = cap.read_u16(msix::MESSAGE_CONTROL); + cap.write_u16(msix::MESSAGE_CONTROL, msix::Msix::function_masked(control)); + } + /// Put MSI back off. /// /// The per-vector mask an arming cleared stays clear: a function whose Mask @@ -454,14 +410,6 @@ impl PciDevice { self.vendor_id() == vendor && self.device_id() == device } - pub fn matches_class(&self, class: u8, subclass: u8, prog_if: Option) -> bool { - if self.mmio.read_u8(CLASS) != class { return false; } - if self.mmio.read_u8(SUBCLASS) != subclass { return false; } - match prog_if { - Some(pi) => self.mmio.read_u8(PROG_IF) == pi, - None => true, - } - } } /// Point a claimed function's [`MSIX_ENTRY`] at the message `claim_msi` made diff --git a/kernel/src/drivers/usb_storage.rs b/kernel/src/drivers/usb_storage.rs deleted file mode 100644 index fb2a979dc61..00000000000 --- a/kernel/src/drivers/usb_storage.rs +++ /dev/null @@ -1,108 +0,0 @@ -//! USB mass storage as a [`BlockDevice`]. Disk state lives in the xHCI -//! controller; this holds only an index and reported geometry, so the -//! controller lock is taken per operation and never held across one. -//! Each `read_blocks`/`write_blocks`/`flush` call opens its own operation -//! budget via [`crate::block::begin_operation`]; `xhci::wait/msc.rs` recovers -//! the deadline from the running context rather than a parameter, since it -//! crosses `BlockAccess`/`BlockDevice` frames that cannot carry it. -//! The guard must stay named `_op`; `let _` would drop it immediately, ending the operation before the call it bounds. - -use alloc::boxed::Box; - -use crate::block::{self, BlockDevice, BlockError, BlockResult, DeviceId, Handle}; -use crate::log; -use super::xhci; - -/// Where USB disks start in the [`DeviceId`] space. -const USB_DEVICE_ID_BASE: DeviceId = 16; - -/// Disk numbers issued this boot; `0..count()` names every bound disk, and a number never moves or is reissued. -pub fn count() -> usize { - xhci::storage_count() -} - -/// The registered handle for the `index`-th disk, registering it on first ask; -/// one device object per disk for the machine's life, so nothing here mints a -/// second answering to a number the block layer has already issued. -pub fn handle(index: usize) -> Option<(Handle, u32)> { - let geometry = xhci::storage_geometry(index)?; - let id = USB_DEVICE_ID_BASE + index as DeviceId; - let handle = match block::open(id) { - Some(handle) => handle, - None => block::register(Box::new(UsbBlockDevice { - index, - id, - blocks: geometry.blocks, - losses: 0, - }))?, - }; - Some((handle, geometry.logical_block_bytes)) -} - -/// Whether the `index`-th disk lost writes by the count `losses` that no -/// writer's flush has reported (`block::Handle::untold`). A disk the block -/// layer never registered had no writer to tell. -pub fn untold(index: usize, losses: u64) -> bool { - block::open(USB_DEVICE_ID_BASE + index as DeviceId).is_some_and(|disk| disk.untold(losses)) -} - -struct UsbBlockDevice { - index: usize, - id: DeviceId, - blocks: u64, - /// The disk's loss count as the device that ran the last operation - /// counted it (`BlockDevice::losses`). - losses: u64, -} - -impl BlockDevice for UsbBlockDevice { - fn device_id(&self) -> DeviceId { - self.id - } - - fn block_count(&self) -> u64 { - self.blocks - } - - fn read_blocks(&mut self, lba: u64, count: u32, buf: &mut [u8]) -> BlockResult { - let _op = block::begin_operation(); - let done = xhci::storage_read(self.index, lba, count, buf, &mut self.losses); - if done.is_err() { - log!("usb-storage: read of {count} blocks at {lba} {} on disk {}", - gave_up(done), self.index); - } - done - } - - fn write_blocks(&mut self, lba: u64, count: u32, buf: &[u8]) -> BlockResult { - let _op = block::begin_operation(); - let done = xhci::storage_write(self.index, lba, count, buf, &mut self.losses); - if done.is_err() { - log!("usb-storage: write of {count} blocks at {lba} {} on disk {}", - gave_up(done), self.index); - } - done - } - - fn flush(&mut self) -> BlockResult { - let _op = block::begin_operation(); - let done = xhci::storage_flush(self.index, &mut self.losses); - if done.is_err() { - log!("usb-storage: cache flush {} on disk {}", gave_up(done), self.index); - } - done - } - - fn losses(&self) -> u64 { - self.losses - } -} - -/// "failed" is a claim about the disk; a budget refusal is a claim about the caller's clock, so the two need different words. -/// Caller must have already matched `done.is_err()`; an `Ok` here would still print "failed". -fn gave_up(done: BlockResult) -> &'static str { - match done { - Err(BlockError::BudgetExpired) => "ran out of its operation budget", - _ => "failed", - } -} diff --git a/kernel/src/drivers/xhci/device.rs b/kernel/src/drivers/xhci/device.rs deleted file mode 100644 index aef6bdef40d..00000000000 --- a/kernel/src/drivers/xhci/device.rs +++ /dev/null @@ -1,913 +0,0 @@ -use core::num::NonZeroU8; - -use crate::log; -use toyos_xhci::enumerate::{ - self, ep0_packet_from_descriptor, initial_ep0_packet, Act, Enumeration, Learnt, Next, Request, -}; -use toyos_xhci::job::{self, Await, Outcome, Stages, CC_SUCCESS}; -use toyos_xhci::port::{self, Reset}; -use toyos_xhci::identity::UsbId; -use toyos_xhci::recovery; -use toyos_xhci::reset_recovery::SlotGoes; -use toyos_abi::inventory::UsbSpeed; -use super::{deadline, Answer, Trb, TrbRing, What, XhciController, PAGE}; -use super::{OFF_INPUT_CTX, OFF_DATA_BUF}; -use super::{DEV_INT_RING, DEV_EP0_RING, DEV_OUT_CTX, DEV_REPORT, EP0_DCI}; -use super::{TRB_ENABLE_SLOT, TRB_ADDRESS_DEVICE, TRB_CONFIGURE_EP, TRB_EVALUATE_CONTEXT}; -use super::enqueue_control; - -use super::hid::{HidType, HidRole, HidDevice}; -use toyos_usbhid::{keyboard, pointer::Pointer}; -use super::msc::{Bind, MscInterface, MscRings}; - -// `wTotalLength` is clamped to this size; the scratch page is four times it. -const MAX_CONFIG_DESC: usize = 256; - -/// One endpoint this driver can configure; only [`Endpoint::new`] can build one, keeping `dci` valid. -#[derive(Clone, Copy)] -pub(super) struct Endpoint { - pub(super) addr: u8, - // 2..=31 by construction; `bind` and `write_ctx32` use it unchecked. - dci: u8, - pub(super) max_packet: u16, - /// The SuperSpeed companion's burst size; zero means one packet per burst. - pub(super) max_burst: u8, - /// bInterval. Only an interrupt endpoint uses it. - pub(super) interval: u8, -} - -impl Endpoint { - pub(super) fn dci(&self) -> u8 { - self.dci - } - - // `None` for endpoint 0 (`0x80`/`0x10`), whose DCI would name the slot or EP0 context. - fn new(addr: u8, max_packet: u16, interval: u8) -> Option { - let num = addr & 0x0F; - (num != 0).then(|| Self { - addr, - dci: num * 2 + u8::from(addr & 0x80 != 0), - max_packet, - max_burst: 0, - interval, - }) - } -} - -#[derive(Clone, Copy)] -struct HidInterfaceInfo { - protocol: HidType, - iface_num: u8, - ep: Endpoint, -} - -/// Always complete: no variant holds an unresolved endpoint; [`Walk`] accumulates until conversion. -#[derive(Clone, Copy)] -enum Function { - Hid(HidInterfaceInfo), - Msc(MscInterface), -} - -impl Function { - // Only whether a boot protocol exists to select is given onward, not endpoints or interface number. - fn shape(self) -> enumerate::Function { - match self { - // A tablet has no boot protocol to select; asking is a request it may stall for. - Self::Hid(info) if info.protocol == HidType::Tablet => enumerate::Function::Hid, - Self::Hid(_) => enumerate::Function::BootHid, - Self::Msc(_) => enumerate::Function::Msc, - } - } -} - -/// Same interface mid-walk; [`Walk::finish`] is the only completeness test into [`Function`]. -enum Walk { - Hid { protocol: HidType, iface_num: u8, ep: Option }, - Msc { iface_num: u8, in_ep: Option, out_ep: Option }, -} - -impl Walk { - // Moves a finished interface into the running answer if this driver can bind it. - fn finish(self, hid: &mut Option, msc: &mut Option) { - match self { - Self::Hid { protocol, iface_num, ep: Some(ep) } => { - if hid.is_none() { - *hid = Some(HidInterfaceInfo { protocol, iface_num, ep }); - } - } - Self::Hid { .. } => {} - Self::Msc { iface_num, in_ep: Some(in_ep), out_ep: Some(out_ep) } => { - if msc.is_none() { - *msc = Some(MscInterface { iface_num, in_ep, out_ep }); - } - } - Self::Msc { iface_num, .. } => { - log!("xHCI: mass-storage interface {iface_num} has no pair of bulk endpoints \ - this driver can configure, skipping"); - } - } - } -} - -// Evaluate Context, not Configure Endpoint: §4.6.7 changes only this field, via the A1 flag. -fn evaluate_ep0_trb(ctrl: &mut XhciController, slot_id: u8, max_packet: u16) -> Trb { - let dma = ctrl.dma(); - let input_ctx = super::zero_dma(dma, OFF_INPUT_CTX, PAGE); - ctrl.write_ctx32(input_ctx, 0, 1, 1 << 1); - let ep0_dw1 = (3u32 << 1) | (4u32 << 3) | ((max_packet as u32) << 16); - ctrl.write_ctx32(input_ctx, 2, 1, ep0_dw1); - - let mut evaluate = Trb::ZERO; - evaluate.param = input_ctx.device_addr(); - evaluate.control = TRB_EVALUATE_CONTEXT | ((slot_id as u32) << 24); - evaluate -} - -// Little-endian 16-bit field at `at`, or 0 past the end; descriptor fields are not aligned. -fn le16(buf: &[u8], at: usize) -> u16 { - let lo = buf.get(at).copied().unwrap_or(0) as u16; - let hi = buf.get(at + 1).copied().unwrap_or(0) as u16; - lo | (hi << 8) -} - -// Bounded by `buf`: every length is device-supplied, and a zero length ends the walk. -fn parse_config(buf: &[u8]) -> Option<(u8, Function)> { - let total_len = (le16(buf, 2) as usize).min(buf.len()); - let config_val = *buf.get(5)?; - - let mut hid: Option = None; - let mut msc: Option = None; - // Which interface the endpoint descriptors that follow belong to. - let mut current: Option = None; - // A SuperSpeed companion describes the endpoint immediately before it. - let mut last_ep_in: Option = None; - - let mut offset = 0usize; - while offset + 2 <= total_len { - let desc_len = buf[offset] as usize; - let desc_type = buf[offset + 1]; - if desc_len == 0 { - break; - } - let desc = match buf.get(offset..(offset + desc_len).min(total_len)) { - Some(d) => d, - None => break, - }; - - match desc_type { - // Interface - 4 if desc.len() >= 9 => { - if let Some(done) = current.take() { - done.finish(&mut hid, &mut msc); - } - let (class, sub, proto) = (desc[5], desc[6], desc[7]); - current = if class == 0x08 && sub == 0x06 && proto == 0x50 { - Some(Walk::Msc { iface_num: desc[2], in_ep: None, out_ep: None }) - } else if class == 3 { - let protocol = match (sub, proto) { - (1, 1) => Some(HidType::Keyboard), - (1, 2) => Some(HidType::Mouse), - (0, _) => Some(HidType::Tablet), - _ => None, - }; - protocol.map(|protocol| Walk::Hid { protocol, iface_num: desc[2], ep: None }) - } else { - None - }; - } - // Endpoint - 5 if desc.len() >= 7 => { - let transfer = desc[3] & 0x3; - last_ep_in = None; - // `if let`, not `let ... else`: the offset advance runs regardless, at the loop's bottom. - if let Some(ep) = Endpoint::new(desc[2], le16(desc, 4), desc[6]) { - let is_in = ep.addr & 0x80 != 0; - match &mut current { - Some(Walk::Hid { ep: slot, .. }) if is_in && slot.is_none() => { - *slot = Some(ep); - } - // Bulk only: an MSC interrupt endpoint belongs to CBI, unsupported here. - Some(Walk::Msc { in_ep, out_ep, .. }) if transfer == 2 => { - if is_in && in_ep.is_none() { - *in_ep = Some(ep); - last_ep_in = Some(true); - } else if !is_in && out_ep.is_none() { - *out_ep = Some(ep); - last_ep_in = Some(false); - } - } - _ => {} - } - } - } - // SuperSpeed Endpoint Companion: states the burst size of the endpoint above it. - 0x30 if desc.len() >= 3 => { - if let (Some(Walk::Msc { in_ep, out_ep, .. }), Some(is_in)) = - (&mut current, last_ep_in) - { - if let Some(ep) = if is_in { in_ep } else { out_ep } { - ep.max_burst = desc[2]; - } - } - } - _ => {} - } - offset += desc_len; - } - if let Some(done) = current { - done.finish(&mut hid, &mut msc); - } - - // MSC wins a tie with HID: a device offering a disk is a disk; `Walk::finish` already guarantees completeness here. - if let Some(m) = msc { - return Some((config_val, Function::Msc(m))); - } - Some((config_val, Function::Hid(hid?))) -} - -/// Asks a port to reset, split from waiting for it because the runtime path must not block a scheduler pass across it. -pub fn reset_port(ctrl: &mut XhciController, port_idx: u8, kind: Reset) { - let portsc = ctrl.read_portsc(port_idx); - ctrl.write_portsc(port_idx, port::reset_write(kind, portsc)); -} - -/// Whether the port has finished the reset it was asked for. -pub fn reset_done(ctrl: &XhciController, port_idx: u8) -> bool { - ctrl.read_portsc(port_idx).reset_finished() -} - -/// One device's enumeration: the state an answer needs, carried because the pass that asked gave up its stack. -pub(super) struct Enumerating { - port_idx: u8, - speed: u8, - /// `speed` decoded once, at [`begin`], where a psiv with no default - /// Protocol Speed ID already refuses the port. - usb_speed: UsbSpeed, - // The enabled slot; every refusal from here carries it back, since only Disable Slot returns one. - slot_id: u8, - block: usize, - ep0_ring: TrbRing, - /// EP0's Max Packet Size as the controller currently believes it. - packet: u16, - seq: Enumeration, - // What an answer means is read against this; `perform` must set it before every submit. - issued: Act, - /// The configuration value and the function the descriptor named. - parsed: Option<(u8, Function)>, - /// What the device descriptor says the device is, and the index of the - /// string that names its serial number (0 for none): a disk's identity. - described: Option<(UsbId, u8)>, - // Carried, not rebuilt: a second `TrbRing::init` would zero memory the controller is already reading. - rings: Option, -} - -/// The transfer rings a device's Configure Endpoint put into the Running state. -#[derive(Clone, Copy)] -enum Rings { - Hid(TrbRing), - Msc(MscRings), -} - - -/// Acknowledges the reset, reads what the port came up as, and asks for a slot — the only act with no device state to carry. -/// -/// `after` is the reset this enumeration follows, and what the acknowledge is a -/// function of; `port::enumeration_ack` is that function, and the simulator's -/// enumerate step calls the same one. -/// -/// **The caller owes a free operation slot**: the Enable Slot below goes into -/// it unasked. The poll's `service_port` defers on `outstanding.wake_at`, and -/// the boot scan's `settle_outstanding` returns only with the slot empty. -pub(super) fn begin(ctrl: &mut XhciController, port_idx: u8, after: Option) { - let portsc = ctrl.read_portsc(port_idx); - ctrl.write_portsc(port_idx, port::enumeration_ack(after, portsc)); - - let portsc = ctrl.read_portsc(port_idx); - // A stale-word guard, not the reset's verdict — `port::reset_outcome` decided that. - if !portsc.enabled() { - log!("xHCI: port {} reset but not enabled (PORTSC {:#010x}); skipping it", - port_idx + 1, portsc.raw()); - return finish(ctrl, port_idx, None); - } - let speed = portsc.speed(); - log!("xHCI: port {} enabled, speed={}", port_idx + 1, speed); - let Some(packet) = initial_ep0_packet(speed) else { - log!("xHCI: port {} came up at speed {speed}, which is not a speed this driver has a \ - control-endpoint packet size for; skipping it", port_idx + 1); - return finish(ctrl, port_idx, None); - }; - // Decoded here, once, so a bound device's inventory record is never built - // from a psiv `UsbSpeed::from_psiv` might refuse: the two tables agreeing - // is checked before a slot is spent, not assumed again every time - // `SYS_DEVICE_INVENTORY` is answered. - let Some(usb_speed) = UsbSpeed::from_psiv(speed) else { - log!("xHCI: port {} came up at speed {speed}, which has a control-endpoint packet size \ - but no default Protocol Speed ID; skipping it", port_idx + 1); - return finish(ctrl, port_idx, None); - }; - - let (seq, _) = Enumeration::begin(); - let mut enable_slot = Trb::ZERO; - enable_slot.control = TRB_ENABLE_SLOT; - let on = Await::Command { trb: ctrl.submit_command(enable_slot) }; - let what = What::SlotWanted { port_idx, speed, usb_speed, packet, seq }; - ctrl.outstanding.submit(what, on, Stages::One, deadline()); -} - -/// Called once Enable Slot answers; everything after this has a device to write for. -pub(super) fn slot_answered( - ctrl: &mut XhciController, - port_idx: u8, - speed: u8, - usb_speed: UsbSpeed, - packet: u16, - seq: Enumeration, - outcome: Outcome, -) { - let Outcome::Command { code: CC_SUCCESS, slot: slot_id } = outcome else { - log!("xHCI: Enable Slot on port {}: {}", port_idx + 1, Answer(outcome)); - return finish(ctrl, port_idx, None); - }; - // A slot id can exceed the pool's device blocks: CONFIG's MaxSlotsEn is only advisory and QEMU ignores it. - // Nothing of the driver's is written yet; refusal below still carries the slot id back. - let Some(block) = ctrl.layout.device(slot_id) else { - log!("xHCI: slot {} is beyond the pool's {} device blocks, dropping port {}", - slot_id, ctrl.layout.dev_blocks, port_idx + 1); - return refuse(ctrl, port_idx, slot_id); - }; - log!("xHCI: slot {} enabled (dma +{:#x})", slot_id, block); - - let ep0_ring = TrbRing::init(ctrl.dma().subview(block + DEV_EP0_RING, PAGE)); - let state = Enumerating { - port_idx, - speed, - usb_speed, - slot_id, - block, - ep0_ring, - packet, - seq, - issued: Act::Command(enumerate::Command::EnableSlot), - parsed: None, - described: None, - rings: None, - }; - advance(ctrl, state, Learnt::Nothing); -} - -/// Reads the outstanding act's answer and either continues the sequence or leaves the port with what it spent. -pub(super) fn stepped(ctrl: &mut XhciController, mut state: Enumerating, outcome: Outcome) { - let port = state.port_idx + 1; - let learnt = match state.issued { - Act::Command(cmd) => { - if !outcome.succeeded() { - log!("xHCI: {} on port {port}: {}", command_name(cmd), Answer(outcome)); - return refuse(ctrl, state.port_idx, state.slot_id); - } - match cmd { - enumerate::Command::AddressDevice => log!("xHCI: device addressed"), - enumerate::Command::ConfigureEndpoint => log!("xHCI: endpoint configured"), - enumerate::Command::SetEp0Dequeue => { - log!("xHCI: EP0 on port {port} runs again after the stall") - } - enumerate::Command::EnableSlot - | enumerate::Command::EvaluateEp0 - | enumerate::Command::ResetEp0 => {} - } - Learnt::Nothing - } - // SET_PROTOCOL is the one request a device may refuse without being refused for it: it still reports in the format its descriptors promised. - Act::Request(Request::SetProtocol) => { - if !outcome.succeeded() { - log!("xHCI: SET_PROTOCOL on port {port}: {}", Answer(outcome)); - // Going on is the decision, but EP0 stays halted until recovery clears it (USB 2.0 §8.5.3.4). - Learnt::Stalled - } else { - Learnt::Nothing - } - } - Act::Request(request) => { - let want = match request { - Request::DeviceDescriptor { want } => want, - Request::ConfigDescriptor => MAX_CONFIG_DESC as u16, - Request::SetConfiguration | Request::SetProtocol => 0, - }; - let Some(delivered) = delivered(outcome, want) else { - log!("xHCI: {} on port {port}: {}", request_name(request), Answer(outcome)); - return refuse(ctrl, state.port_idx, state.slot_id); - }; - match read_back(ctrl, &mut state, request, delivered) { - Ok(learnt) => learnt, - Err(()) => return refuse(ctrl, state.port_idx, state.slot_id), - } - } - }; - advance(ctrl, state, learnt); -} - -/// Ask the sequence what is owed next and do it. -fn advance(ctrl: &mut XhciController, state: Enumerating, learnt: Learnt) { - match state.seq.completed(learnt) { - Next::Act(seq, act) => perform(ctrl, Enumerating { seq, ..state }, act), - Next::Bind => bind(ctrl, state), - Next::Refuse => { - log!("xHCI: no HID boot interface found on port {}, skipping it", - state.port_idx + 1); - refuse(ctrl, state.port_idx, state.slot_id) - } - } -} - -/// Submit one act and record that the controller owes an answer for it. -fn perform(ctrl: &mut XhciController, mut state: Enumerating, act: Act) { - state.issued = act; - let submitted = match act { - Act::Command(cmd) => command(ctrl, &mut state, cmd) - .map(|trb| (Await::Command { trb: ctrl.submit_command(trb) }, Stages::One)), - Act::Request(request) => Some(control(ctrl, &mut state, request)), - }; - let Some((on, stages)) = submitted else { - // A Configure Endpoint is refused only for want of a pool block, which - // a held disk's teardown may be about to give back. - if act == Act::Command(enumerate::Command::ConfigureEndpoint) { - return refuse_for_now(ctrl, state.port_idx, state.slot_id); - } - return refuse(ctrl, state.port_idx, state.slot_id); - }; - ctrl.outstanding.submit(What::Enumerating(state), on, stages, deadline()); -} - -/// The TRB for one command act, or `None` where the driver has already refused it. -fn command( - ctrl: &mut XhciController, - state: &mut Enumerating, - cmd: enumerate::Command, -) -> Option { - match cmd { - // Answered in `begin`; never asked for again. - enumerate::Command::EnableSlot => None, - enumerate::Command::AddressDevice => Some(address_device_trb(ctrl, state)), - enumerate::Command::EvaluateEp0 => { - Some(evaluate_ep0_trb(ctrl, state.slot_id, state.packet)) - } - enumerate::Command::ConfigureEndpoint => configure_endpoint_trb(ctrl, state), - // EP0's own stall recovery, via the same `recovery_trb` builder bulk/interrupt endpoints use; its Set TR Dequeue re-points the ring off the stalled TRB. - enumerate::Command::ResetEp0 => Some(ctrl.recovery_trb( - recovery::Command::ResetEndpoint, - state.slot_id, - EP0_DCI, - &mut state.ep0_ring, - state.block + DEV_EP0_RING, - )), - enumerate::Command::SetEp0Dequeue => Some(ctrl.recovery_trb( - recovery::Command::SetDequeue, - state.slot_id, - EP0_DCI, - &mut state.ep0_ring, - state.block + DEV_EP0_RING, - )), - } -} - -// A request with a data stage is two completions; `enqueue_control` already accounts for both. -fn control( - ctrl: &mut XhciController, - state: &mut Enumerating, - request: Request, -) -> (Await, Stages) { - let dma = ctrl.dma(); - let scratch = dma.device_addr() + OFF_DATA_BUF as u64; - let (bm_request_type, b_request, w_value, w_index, data, len) = match request { - Request::DeviceDescriptor { want } => (0x80, 0x06, 0x0100, 0, Some(scratch), want), - Request::ConfigDescriptor => { - (0x80, 0x06, 0x0200, 0, Some(scratch), MAX_CONFIG_DESC as u16) - } - Request::SetConfiguration => { - let (config_val, _) = state.parsed.expect("a configuration named a function"); - (0x00, 0x09, config_val as u16, 0, None, 0) - } - Request::SetProtocol => { - let iface = match state.parsed { - Some((_, Function::Hid(info))) => info.iface_num, - _ => unreachable!("only a HID interface has a boot protocol to select"), - }; - (0x21, 0x0B, 0, iface as u16, None, 0) - } - }; - // The scratch page is shared and enumeration is serial; zeroed first so a short answer can't leak stale data. - if data.is_some() { - super::zero_dma(dma, OFF_DATA_BUF, MAX_CONFIG_DESC); - } - let trbs = enqueue_control( - &mut state.ep0_ring, bm_request_type, b_request, w_value, w_index, data, len, - ); - ctrl.ring_doorbell(state.slot_id, 1); - trbs.awaits(state.slot_id) -} - -/// What a completed control request left in the scratch page; `Err` is a device already refused. -fn read_back( - ctrl: &mut XhciController, - state: &mut Enumerating, - request: Request, - delivered: u16, -) -> Result { - let port = state.port_idx + 1; - // A bounded copy, not a borrow: every read below indexes this slice, so a device-chosen length can't walk past it. - // Safe to copy now: `control` zeroed this page before the transfer, and `read_back` runs only once that transfer completes. - let mut scratch = [0u8; MAX_CONFIG_DESC]; - ctrl.dma().copy_to(OFF_DATA_BUF, &mut scratch); - let scratch: &[u8] = &scratch; - match request { - Request::DeviceDescriptor { want: 8 } => { - if delivered < 8 { - log!("xHCI: port {port} would not give up the first 8 bytes of its device \ - descriptor: {delivered} B delivered"); - return Err(()); - } - let stated = scratch[7]; - let Some(ep0_packet) = ep0_packet_from_descriptor(state.speed, stated) else { - log!("xHCI: port {port} states bMaxPacketSize0={stated}, which is not a control \ - packet size a speed-{} device has; skipping it", state.speed); - return Err(()); - }; - if ep0_packet == state.packet { - return Ok(Learnt::Nothing); - } - log!("xHCI: port {port} EP0 packet size {} -> {ep0_packet}", state.packet); - state.packet = ep0_packet; - Ok(Learnt::Ep0PacketWrong) - } - Request::DeviceDescriptor { .. } => { - if delivered < 18 { - log!("xHCI: GET_DESCRIPTOR(Device) on port {port}: {delivered} B delivered"); - return Err(()); - } - let descriptor = &scratch[..18]; - log!("xHCI: device class={:#x} vendor={:04x} product={:04x}", - descriptor[4], le16(descriptor, 8), le16(descriptor, 10)); - let usb = UsbId { - vendor: le16(descriptor, 8), - product: le16(descriptor, 10), - release: le16(descriptor, 12), - }; - state.described = Some((usb, descriptor[16])); - Ok(Learnt::Nothing) - } - // Nine bytes is the header holding `wTotalLength`; the parser is bounded by what arrived, not what was asked. - Request::ConfigDescriptor => { - if delivered < 9 { - log!("xHCI: port {port} answered {delivered} B to GET_DESCRIPTOR(Config); a \ - configuration descriptor is at least 9", ); - return Err(()); - } - // `delivered` bounds the parse; the `min` refuses a device claiming more than the page holds. - let config = &scratch[..(delivered as usize).min(scratch.len())]; - let Some((config_val, function)) = parse_config(config) else { - return Ok(Learnt::Nothing); - }; - match function { - Function::Msc(msc) => log!("xHCI: mass storage iface={} in={:#x}/{} out={:#x}/{}", - msc.iface_num, msc.in_ep.addr, msc.in_ep.max_packet, - msc.out_ep.addr, msc.out_ep.max_packet), - Function::Hid(info) => log!("xHCI: HID {} iface={} ep={:#x} max_pkt={} \ - interval={} dci={}", hid_kind(info.protocol), info.iface_num, info.ep.addr, - info.ep.max_packet, info.ep.interval, info.ep.dci()), - } - state.parsed = Some((config_val, function)); - Ok(Learnt::Function(function.shape())) - } - Request::SetConfiguration => { - log!("xHCI: configuration set"); - Ok(Learnt::Nothing) - } - Request::SetProtocol => Ok(Learnt::Nothing), - } -} - -/// Bytes a control request actually moved, or `None` if it did not complete; Success alone doesn't say. -fn delivered(outcome: Outcome, want: u16) -> Option { - let Outcome::Transfer { code: CC_SUCCESS, residue } = outcome else { return None }; - // No more than `want`, so it is a `u16`. - Some(job::moved(want.into(), residue) as u16) -} - -fn command_name(cmd: enumerate::Command) -> &'static str { - match cmd { - enumerate::Command::EnableSlot => "Enable Slot", - enumerate::Command::AddressDevice => "Address Device", - enumerate::Command::EvaluateEp0 => "Evaluate Context (EP0 packet size)", - enumerate::Command::ConfigureEndpoint => "Configure Endpoint", - enumerate::Command::ResetEp0 => "Reset Endpoint (EP0)", - enumerate::Command::SetEp0Dequeue => "Set TR Dequeue Pointer (EP0)", - } -} - -fn request_name(request: Request) -> &'static str { - match request { - Request::DeviceDescriptor { want: 8 } => "GET_DESCRIPTOR(Device, 8)", - Request::DeviceDescriptor { .. } => "GET_DESCRIPTOR(Device)", - Request::ConfigDescriptor => "GET_DESCRIPTOR(Config)", - Request::SetConfiguration => "SET_CONFIGURATION", - Request::SetProtocol => "SET_PROTOCOL", - } -} - -fn hid_kind(protocol: HidType) -> &'static str { - match protocol { - HidType::Keyboard => "keyboard", - HidType::Mouse => "mouse", - HidType::Tablet => "tablet", - } -} - -/// The Address Device command, with the slot and control endpoint this device will be known by. -fn address_device_trb(ctrl: &mut XhciController, state: &Enumerating) -> Trb { - let dma = ctrl.dma(); - let input_ctx = super::zero_dma(dma, OFF_INPUT_CTX, PAGE); - - ctrl.write_ctx32(input_ctx, 0, 1, 0x3); // Add Slot + EP0 - let slot_dw0 = ((state.speed as u32) << 20) | (1u32 << 27); - ctrl.write_ctx32(input_ctx, 1, 0, slot_dw0); - ctrl.write_ctx32(input_ctx, 1, 1, (state.port_idx as u32 + 1) << 16); - - let ep0_dw1 = (3u32 << 1) | (4u32 << 3) | ((state.packet as u32) << 16); - ctrl.write_ctx32(input_ctx, 2, 1, ep0_dw1); - let ep0_dequeue = state.ep0_ring.dequeue(); - ctrl.write_ctx32(input_ctx, 2, 2, ep0_dequeue as u32); - ctrl.write_ctx32(input_ctx, 2, 3, (ep0_dequeue >> 32) as u32); - ctrl.write_ctx32(input_ctx, 2, 4, 8); - - let out_ctx = super::zero_dma(dma, state.block + DEV_OUT_CTX, PAGE / 2); - ctrl.write_dcbaa(state.slot_id as usize, out_ctx.device_addr()); - - let mut addr_dev = Trb::ZERO; - addr_dev.param = input_ctx.device_addr(); - addr_dev.control = TRB_ADDRESS_DEVICE | ((state.slot_id as u32) << 24); - addr_dev -} - -/// The Configure Endpoint command, with its transfer rings recorded; `None` is a refusal — a disk past the pool's mass-storage blocks — not a failure. -fn configure_endpoint_trb(ctrl: &mut XhciController, state: &mut Enumerating) -> Option { - let (_, function) = state.parsed.expect("a configuration named a function"); - let rings = match function { - Function::Hid(info) => Rings::Hid(hid_input_context(ctrl, state, &info)), - Function::Msc(info) => Rings::Msc(super::msc::prepare( - ctrl, state.slot_id, state.speed, state.port_idx, &info, - )?), - }; - state.rings = Some(rings); - - let mut configure = Trb::ZERO; - configure.param = ctrl.dma().subview(OFF_INPUT_CTX, PAGE).device_addr(); - configure.control = TRB_CONFIGURE_EP | ((state.slot_id as u32) << 24); - Some(configure) -} - -/// The input context for one HID interrupt endpoint, and the ring it runs on. -fn hid_input_context( - ctrl: &mut XhciController, - state: &Enumerating, - info: &HidInterfaceInfo, -) -> TrbRing { - let dma = ctrl.dma(); - let int_ep_dci = info.ep.dci(); - let int_ring = TrbRing::init(dma.subview(state.block + DEV_INT_RING, PAGE)); - - let input_ctx = super::zero_dma(dma, OFF_INPUT_CTX, PAGE); - - ctrl.write_ctx32(input_ctx, 0, 1, (1u32 << (int_ep_dci as u32)) | 1); - - let slot_dw0 = ((state.speed as u32) << 20) | ((int_ep_dci as u32) << 27); - ctrl.write_ctx32(input_ctx, 1, 0, slot_dw0); - ctrl.write_ctx32(input_ctx, 1, 1, (state.port_idx as u32 + 1) << 16); - - let ep_ctx_index = int_ep_dci as usize + 1; - let interval_val = if info.ep.interval == 0 { 0u32 } else if state.speed <= 2 { - let frames = (info.ep.interval as u32) * 8; - let mut exp = 0u32; - let mut v = frames; - while v > 1 { v >>= 1; exp += 1; } - exp - } else { - (info.ep.interval - 1) as u32 - }; - ctrl.write_ctx32(input_ctx, ep_ctx_index, 0, interval_val << 16); - - let ep_dw1 = (3u32 << 1) | (7u32 << 3) | ((info.ep.max_packet as u32) << 16); - ctrl.write_ctx32(input_ctx, ep_ctx_index, 1, ep_dw1); - - let int_dequeue = int_ring.dequeue(); - ctrl.write_ctx32(input_ctx, ep_ctx_index, 2, int_dequeue as u32); - ctrl.write_ctx32(input_ctx, ep_ctx_index, 3, (int_dequeue >> 32) as u32); - // Max ESIT Payload (high) / Average TRB Length (low) are both the max packet size (xHCI 1.2 §6.2.3.8, §4.14.2). - let esit = info.ep.max_packet as u32; - ctrl.write_ctx32(input_ctx, ep_ctx_index, 4, (esit << 16) | esit); - int_ring -} - -// Everything class-specific: where this sequence stops and the device's own driver starts. -fn bind(ctrl: &mut XhciController, state: Enumerating) { - let (_, function) = state.parsed.expect("a configuration named a function"); - let rings = state.rings.expect("Configure Endpoint named this device's rings"); - // Whether a device came of it decides who keeps the slot; a refusal here would leak it. - let keeps_slot = match (function, rings) { - (Function::Msc(info), Rings::Msc(msc)) => { - let (configuration, _) = state.parsed.expect("a configuration named a function"); - let enumerated = super::msc::Enumerated { - speed: state.speed, - usb_speed: state.usb_speed, - ep0_packet: state.packet, - configuration, - }; - // The full descriptor is read before the configuration is, so a - // device that reached here gave one. - let described = state.described.expect("the device descriptor was read before its configuration"); - match super::msc::bind( - ctrl, state.ep0_ring, state.slot_id, state.block, msc, &info, enumerated, described, - ) { - Bind::Bound => true, - Bind::NotReady => return refuse_for_now(ctrl, state.port_idx, state.slot_id), - Bind::Refused(SlotGoes::Back) => false, - // No device came of it, and its bulk pair could not be Stopped: Disable Slot is not defined over it (xHCI 1.2 §4.6.4's note), so the port holds the slot and its teardown gives it back. - Bind::Refused(SlotGoes::WithTheUnplug) => true, - } - } - (Function::Hid(info), Rings::Hid(int_ring)) => bind_hid(ctrl, &state, &info, int_ring), - // Rings are built from the function two acts earlier; nothing between can change it. - _ => unreachable!("the rings were built for another function"), - }; - if keeps_slot { - finish(ctrl, state.port_idx, Some(state.slot_id)); - } else { - refuse(ctrl, state.port_idx, state.slot_id); - } -} - -/// `true` if a device came of it — the caller gives the slot back if not. -fn bind_hid( - ctrl: &mut XhciController, - state: &Enumerating, - info: &HidInterfaceInfo, - int_ring: TrbRing, -) -> bool { - let report = ctrl.dma().subview(state.block + DEV_REPORT, 8); - let pointer = match info.protocol { - HidType::Keyboard => None, - HidType::Mouse => Some(Pointer::Mouse), - HidType::Tablet => Some(Pointer::Tablet), - }; - 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 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 => { - log!("xHCI: slot {} is past the pointers this machine can number, dropping it", - state.slot_id); - return false; - } - }, - }; - let mut dev = HidDevice { - slot_id: state.slot_id, - port_idx: state.port_idx, - speed: state.usb_speed, - usb: state.described.expect("the device descriptor was read before its configuration").0, - block: state.block, - int_ep_dci: info.ep.dci(), - ep_addr: info.ep.addr, - int_ring, - ep0_ring: state.ep0_ring, - report, - report_size, - role, - refused: 0, - rolled_over: 0, - broke_with: None, - failures: 0, - }; - - dev.requeue(&ctrl.db_base); - // The ring offset is logged because two devices sharing one ring is invisible until their TRBs interleave. - log!("xHCI: USB {} ready on slot {}, int_ring +{:#x}", - hid_kind(info.protocol), state.slot_id, state.block + DEV_INT_RING); - // Logged because a source derived from the slot id would merge two controllers' slot-1 devices into one. - if let HidRole::Pointer(source, _) = dev.role { - log!("xHCI: pointer on slot {} merges as source {}", state.slot_id, source.id()); - } - ctrl.devices.push(dev); - true -} - -/// The enumeration is over; `slot` is recorded even on refusal, since only Disable Slot returns it. -pub(super) fn finish(ctrl: &mut XhciController, port_idx: u8, slot: Option) { - ctrl.ports[port_idx as usize].enumerated(slot.and_then(NonZeroU8::new)); - // Clears flags other than PRC left set on a now-quiet port, so a later disconnect's CSC isn't already '1'. - ctrl.acknowledge_port_read(port_idx); -} - -/// Refuses the device, with the port left attached and no slot so it isn't re-enumerated next debounce. -pub(super) fn refuse(ctrl: &mut XhciController, port_idx: u8, slot_id: u8) { - ctrl.ports[port_idx as usize].enumerated(None); - ctrl.acknowledge_port_read(port_idx); - // Released here, not at unplug: a refused device left plugged in would otherwise hold a slot for the rest of the boot. - ctrl.submit_disable_slot(slot_id, super::AfterSlot::Refused); -} - -/// [`refuse`], for a disk refused for a reason a later look may not find: while a disk on this controller is held for its device, the one refused may be that device, so its port is enumerated again once its slot is back (`AfterSlot::Again`). Bounded by the held disk's window, after which a refusal is [`refuse`]'s. -fn refuse_for_now(ctrl: &mut XhciController, port_idx: u8, slot_id: u8) { - if !ctrl.awaits_a_device() { - return refuse(ctrl, port_idx, slot_id); - } - ctrl.ports[port_idx as usize].enumerated(None); - ctrl.acknowledge_port_read(port_idx); - ctrl.submit_disable_slot(slot_id, super::AfterSlot::Again(port_idx)); -} - -/// Drops the outstanding enumeration for a port whose device has gone; the slot passes to the port for teardown. -pub(super) fn cancel_on(ctrl: &mut XhciController, port_idx: u8) { - // An outstanding Enable Slot is left alone: its answer is the only carrier of the slot id, and cancelling here would leak it. - let Some(What::Enumerating(state)) = ctrl.outstanding.what() else { return }; - if state.port_idx != port_idx { - return; - } - let slot_id = state.slot_id; - ctrl.outstanding.cancel(); - log!("xHCI: the enumeration on port {} is abandoned; its device has gone", port_idx + 1); - // Only the slot, not `finish`'s acknowledge: that would clear the very change flag that brought us here. - ctrl.ports[port_idx as usize].enumerated(NonZeroU8::new(slot_id)); -} - -/// Exercises configuration descriptors no attached device will ever hand us, since `parse_config` is pure. -#[cfg(feature = "boot-actuators")] -pub fn selftest() { - /// (kind, config value, first DCI, second DCI); a tuple because `Function` has no equality. - type Verdict = Option<(u8, u8, u8, u8)>; - - fn summarise(got: Option<(u8, Function)>) -> Verdict { - match got? { - (cfg, Function::Hid(h)) => Some((1, cfg, h.ep.dci(), 0)), - (cfg, Function::Msc(m)) => Some((2, cfg, m.in_ep.dci(), m.out_ep.dci())), - } - } - - /// A config descriptor with `wTotalLength = total` and body: one interface then `eps` (address, transfer type). - fn build(buf: &mut [u8; 64], class: (u8, u8, u8), eps: &[(u8, u8)], total: u16) -> usize { - buf.fill(0); - buf[..9].copy_from_slice(&[9, 2, total as u8, (total >> 8) as u8, 1, 0x42, 0, 0, 0]); - buf[9..18].copy_from_slice(&[9, 4, 0, 0, eps.len() as u8, class.0, class.1, class.2, 0]); - let mut at = 18; - for &(addr, transfer) in eps { - buf[at..at + 7].copy_from_slice(&[7, 5, addr, transfer, 64, 0, 8]); - at += 7; - } - at - } - - const MSC: (u8, u8, u8) = (0x08, 0x06, 0x50); - const KBD: (u8, u8, u8) = (3, 1, 1); - const CASES: usize = 9; - - let mut passed = 0usize; - let mut buf = [0u8; 64]; - let mut check = |name: &str, desc: &[u8], want: Verdict| { - let got = summarise(parse_config(desc)); - if got == want { - passed += 1; - } else { - log!("xHCI: descriptor selftest FAILED on {name}: got {got:?}, want {want:?}"); - } - }; - - let len = build(&mut buf, MSC, &[(0x81, 2), (0x02, 2)], 32); - check("an ordinary disk", &buf[..len], Some((2, 0x42, 3, 4))); - - let len = build(&mut buf, MSC, &[(0x80, 2), (0x02, 2)], 32); - check("a bulk IN endpoint naming endpoint 0", &buf[..len], None); - - let len = build(&mut buf, MSC, &[(0x81, 2), (0x10, 2)], 32); - check("a bulk OUT endpoint naming endpoint 0", &buf[..len], None); - - let len = build(&mut buf, MSC, &[(0x81, 3), (0x02, 3)], 32); - check("a mass-storage interface with no bulk pair", &buf[..len], None); - - let len = build(&mut buf, KBD, &[(0x81, 3)], 25); - check("an ordinary keyboard", &buf[..len], Some((1, 0x42, 3, 0))); - - let len = build(&mut buf, KBD, &[(0x80, 3)], 25); - check("a keyboard whose interrupt endpoint is endpoint 0", &buf[..len], None); - - let len = build(&mut buf, KBD, &[(0x81, 3)], 25); - buf[9] = 0; - check("a descriptor claiming zero length", &buf[..len], None); - - let len = build(&mut buf, KBD, &[(0x81, 3)], u16::MAX); - check("wTotalLength past the buffer", &buf[..len], Some((1, 0x42, 3, 0))); - - let len = build(&mut buf, KBD, &[(0x81, 3)], 25); - check("a truncated final descriptor", &buf[..len - 3], None); - - log!("xHCI: descriptor selftest {passed}/{CASES} configurations parsed as required"); -} diff --git a/kernel/src/drivers/xhci/hid.rs b/kernel/src/drivers/xhci/hid.rs deleted file mode 100644 index 64b5c39c3a8..00000000000 --- a/kernel/src/drivers/xhci/hid.rs +++ /dev/null @@ -1,156 +0,0 @@ - -use toyos_usbhid::keyboard::{Keyboard, Read}; -use toyos_usbhid::pointer::{Motion, Pointer}; -use toyos_xhci::job; - -use crate::{keyboard, log, mouse}; -use super::{Mmio, Trb, TrbRing, TRB_NORMAL}; - -/// What a configuration descriptor's HID interface said it was, differing only in report size and SET_PROTOCOL; parse-time only. -#[derive(Clone, Copy, PartialEq)] -pub enum HidType { - Keyboard, - Mouse, - Tablet, -} - -/// What a *bound* device is, with what its reports are read against: a keyboard's own last report, since diffing against another device's would synthesize releases for keys still down; and a pointer's source, carried rather than derived from the (per-controller) slot id. -pub enum HidRole { - Keyboard(Keyboard), - Pointer(mouse::PointerSource, Pointer), -} - -pub struct HidDevice { - pub slot_id: u8, - /// The root-hub port this device is on; unlike the slot id, survives a disable. - pub port_idx: u8, - /// PORTSC's Port Speed when the port came up, decoded once at bind: a - /// psiv with no default Protocol Speed ID refused the port before a slot - /// was ever spent on it. - pub speed: toyos_abi::inventory::UsbSpeed, - /// What its device descriptor says it is. - pub usb: toyos_xhci::identity::UsbId, - /// This device's block in the DMA pool: interrupt ring, EP0 ring and output context. - pub block: usize, - pub int_ep_dci: u8, - /// The device's own endpoint address, distinct from the controller's DCI; what CLEAR_FEATURE(ENDPOINT_HALT) needs. - pub ep_addr: u8, - pub int_ring: TrbRing, - /// The device's control ring, kept past enumeration: clearing a halt is a control transfer. - pub ep0_ring: TrbRing, - /// The eight-byte DMA slot the interrupt endpoint delivers reports into; a Dma view bounds accesses against its own length, not `report_size`. - pub report: crate::mm::Dma<'static>, - pub report_size: u32, - pub role: HidRole, - /// Reports refused since bind, none of them believed; logged as [`tally`] says. - pub refused: u32, - /// Keyboard reports in rollover since bind, each believed for its modifiers alone; logged as [`tally`] says. - pub rolled_over: u32, - /// The completion code this endpoint broke with; read and cleared by [`super::XhciController::recover_endpoints`], never by the code that sets it. - pub broke_with: Option, - /// Consecutive failures; a delivered report clears it — see [`super::MAX_HID_FAILURES`]. - pub failures: u8, -} - -impl HidDevice { - /// What this device is called in every line about it: two names, not three — mouse and tablet dispatch identically, distinguished only by report length. - pub fn kind(&self) -> &'static str { - match self.role { - HidRole::Keyboard(_) => "keyboard", - HidRole::Pointer(..) => "pointer", - } - } - - /// Reads the report a transfer delivered; `residue` is what of `report_size` the device did not send, whose bytes are still the last report's. - pub fn dispatch_report(&mut self, residue: u32) { - let mut buf = [0u8; 8]; - // `report_size` is 4, 6 or 8, so `copy_to` never sees more than 8; not yet requeued, so this copy has the buffer to itself. - let delivered = &mut buf[..job::moved(self.report_size, residue) as usize]; - self.report.copy_to(0, delivered); - // Waking on an unchanged report would make readiness disagree with `has_data()`. - let queued = match &mut self.role { - HidRole::Keyboard(keys) => match keys.report(delivered) { - Ok(Read::Keys(transitions)) => keyboard::apply(transitions) != 0, - Ok(Read::RollOver(transitions)) => { - let queued = keyboard::apply(transitions) != 0; - self.roll_over(); - queued - } - Err(why) => return self.refuse(format_args!("{why:?}")), - }, - HidRole::Pointer(source, pointer) => match pointer.decode(delivered) { - Ok(read) => { - mouse::report(*source, read.buttons, motion(read.motion), read.wheel); - true - } - Err(why) => return self.refuse(format_args!("{why:?}")), - }, - }; - if queued { - self.wake(); - } - } - - #[cold] - fn refuse(&mut self, why: core::fmt::Arguments<'_>) { - if tally(&mut self.refused) { - log!("xHCI: slot {} {} report refused, {} since it bound: {why}", - self.slot_id, self.kind(), self.refused); - } - } - - #[cold] - fn roll_over(&mut self) { - if tally(&mut self.rolled_over) { - log!("xHCI: slot {} keyboard report in rollover, its modifiers believed and its other keys held, \ - {} since it bound", self.slot_id, self.rolled_over); - } - } - - /// Releases everything this device was holding, on its way off the bus. - /// 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 source, so replugging costs the machine nothing. - HidRole::Pointer(source, _) => { - mouse::unbind(*source); - true - } - }; - if queued { - self.wake(); - } - } - - // The role's one watch, which carries the blocked `sys_read` and the polls alike. - fn wake(&self) { - match self.role { - HidRole::Keyboard(_) => crate::keyboard::WATCH.post(), - HidRole::Pointer(..) => crate::mouse::WATCH.post(), - } - } - - pub fn requeue(&mut self, db_base: &Mmio) { - let mut trb = Trb::ZERO; - trb.param = self.report.device_addr(); - trb.status = self.report_size; - trb.control = TRB_NORMAL | (1 << 5); // IOC - self.int_ring.enqueue(trb); - // An `Mmio` write: ordered after the TRB it announces. - db_base.write_u32(self.slot_id as u64 * 4, self.int_ep_dci as u32); - } -} - -/// Counts one more; true for the first and each power of two after it, so a device cannot flood the log. -fn tally(count: &mut u32) -> bool { - *count = count.saturating_add(1); - count.is_power_of_two() -} - -fn motion(motion: Motion) -> mouse::Motion { - match motion { - Motion::Relative { dx, dy } => mouse::Motion::Relative { dx: dx.into(), dy: dy.into() }, - Motion::Absolute { x, y } => mouse::Motion::Absolute { x, y }, - } -} diff --git a/kernel/src/drivers/xhci/legacy.rs b/kernel/src/drivers/xhci/legacy.rs deleted file mode 100644 index 800a633dfc1..00000000000 --- a/kernel/src/drivers/xhci/legacy.rs +++ /dev/null @@ -1,246 +0,0 @@ -//! Takes the xHC from firmware before reset (xHCI spec §4.22.1). -//! -//! The extended capability list is firmware's, so it is untrusted: nothing -//! here panics or loops unbounded on firmware-supplied data, and a malformed -//! list costs the handoff, never the boot. - -use crate::log; -use crate::mm::Mmio; -use crate::time::{Budget, Duration}; - -/// Capability ID 1 — USB Legacy Support (spec §7.1.1). -const CAP_ID_LEGACY: u8 = 1; - -/// USBLEGSUP bit 16: firmware owns the controller. -const LEGSUP_BIOS_OWNED: u32 = 1 << 16; -/// USBLEGSUP bit 24: the OS claims it. -const LEGSUP_OS_OWNED: u32 = 1 << 24; - -/// USBLEGCTLSTS sits one dword past USBLEGSUP (spec §7.1.2). -const LEGCTLSTS: u64 = 4; -/// Its SMI enables: USB SMI, host-system-error, OS-ownership, PCI-command, BAR. -const SMI_ENABLES: u32 = (1 << 0) | (1 << 4) | (1 << 13) | (1 << 14) | (1 << 15); -/// Its write-1-to-clear SMI status bits: OS ownership change, PCI command, BAR. -const SMI_STATUS: u32 = (1 << 29) | (1 << 30) | (1 << 31); - -/// Grace period for firmware to release the controller; expiry is not fatal. -const HANDOFF_TIMEOUT: Budget = Budget::of( - Duration::from_secs(1), - "the controller is reset out from under firmware, with a line naming the semaphore", -); - -/// Iteration cap for the capability walk, independent of firmware's link count. -const MAX_CAPS: usize = 64; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum WalkError { - /// A capability header outside the mapped register window. - OutOfWindow(u64), - /// More links than `MAX_CAPS`. - TooMany, -} - -/// Byte offset of the first capability with `id`, or `None` if the list has none. -/// `read` must return `None` for any offset outside the mapped window. -pub fn find( - read: &dyn Fn(u64) -> Option, - xecp_dwords: u32, - id: u8, -) -> Result, WalkError> { - let mut found = None; - walk(read, xecp_dwords, id, &mut |at| { - found = Some(at); - false - })?; - Ok(found) -} - -/// Every capability with `id`, in list order; an id may appear more than once. -pub fn for_each( - read: &dyn Fn(u64) -> Option, - xecp_dwords: u32, - id: u8, - visit: &mut dyn FnMut(u64), -) -> Result<(), WalkError> { - walk(read, xecp_dwords, id, &mut |at| { - visit(at); - true - }) -} - -/// `visit` returns `false` to stop the walk early. -fn walk( - read: &dyn Fn(u64) -> Option, - xecp_dwords: u32, - id: u8, - visit: &mut dyn FnMut(u64) -> bool, -) -> Result<(), WalkError> { - if xecp_dwords == 0 { - return Ok(()); - } - let mut offset = xecp_dwords as u64 * 4; - for _ in 0..MAX_CAPS { - let Some(header) = read(offset) else { - return Err(WalkError::OutOfWindow(offset)); - }; - if header as u8 == id && !visit(offset) { - return Ok(()); - } - let next = (header >> 8) & 0xFF; - if next == 0 { - return Ok(()); - } - // Offset strictly increases (1..=255 dwords per step) and cannot wrap. - offset += next as u64 * 4; - } - Err(WalkError::TooMany) -} - -/// Capability ID 2 — Supported Protocol (spec §7.2). -pub const CAP_ID_PROTOCOL: u8 = 2; - -/// Asks firmware for the controller and disables its SMIs regardless of the answer. -/// Never refuses: the caller resets the controller whether or not the handoff succeeded. -pub fn take_ownership(bar: &Mmio, bar_size: u64, hccparams1: u32) { - #[cfg(feature = "boot-actuators")] - if crate::actuator::xhci_xecp_selftest() { - selftest(); - } - - let xecp = hccparams1 >> 16; - let read = |offset: u64| -> Option { - (offset.checked_add(4)? <= bar_size).then(|| bar.read_u32(offset)) - }; - let legsup = match find(&read, xecp, CAP_ID_LEGACY) { - Ok(Some(offset)) => offset, - Ok(None) => { - log!("xHCI: no USB Legacy Support capability (xECP={xecp:#x}), nothing to hand over"); - return; - } - Err(e) => { - log!("xHCI: extended capability list unusable ({e:?}, xECP={xecp:#x}) — no handoff"); - return; - } - }; - // USBLEGCTLSTS is the dword after the one `find` bounds-checked. - if legsup + LEGCTLSTS + 4 > bar_size { - log!("xHCI: USB Legacy Support at {legsup:#x} runs past the register window — no handoff"); - return; - } - - let before = bar.read_u32(legsup); - bar.write_u32(legsup, before | LEGSUP_OS_OWNED); - - let started = crate::clock::nanos_since_boot(); - let deadline = started + HANDOFF_TIMEOUT.nanos(); - let mut now = bar.read_u32(legsup); - while now & LEGSUP_BIOS_OWNED != 0 && crate::clock::nanos_since_boot() < deadline { - core::hint::spin_loop(); - now = bar.read_u32(legsup); - } - let waited_us = (crate::clock::nanos_since_boot() - started) / 1_000; - - if now & LEGSUP_BIOS_OWNED != 0 { - // Not fatal: the controller resets regardless of what firmware reports. - log!( - "xHCI: firmware still owns the controller after {}ms (USBLEGSUP {before:#010x} -> {now:#010x}) — resetting it anyway", - HANDOFF_TIMEOUT.duration().millis() - ); - } else if before & LEGSUP_BIOS_OWNED != 0 { - log!("xHCI: firmware released the controller in {waited_us}us (USBLEGSUP {before:#010x} -> {now:#010x})"); - } else { - log!("xHCI: firmware did not claim the controller (USBLEGSUP {before:#010x})"); - } - - // SMIs are disabled whatever the ownership result; the two bits are independent. - let ctl = bar.read_u32(legsup + LEGCTLSTS); - bar.write_u32(legsup + LEGCTLSTS, (ctl & !SMI_ENABLES) | SMI_STATUS); - let after = bar.read_u32(legsup + LEGCTLSTS); - log!("xHCI: USBLEGCTLSTS {ctl:#010x} -> {after:#010x} (SMI generation off)"); -} - -/// Malformed capability lists QEMU's xHC cannot produce; exercises [`find`]'s bounds checks. -#[cfg(feature = "boot-actuators")] -fn selftest() { - /// A 16-dword register window, so a dword at or past 0x40 is outside it. - const WINDOW: u64 = 64; - const CASES: usize = 8; - - fn header(id: u8, next: u8) -> u32 { - id as u32 | ((next as u32) << 8) - } - - let mut passed = 0usize; - let mut check = |name: &str, - read: &dyn Fn(u64) -> Option, - xecp: u32, - want: Result, WalkError>| { - let got = find(read, xecp, CAP_ID_LEGACY); - if got == want { - passed += 1; - } else { - log!("xHCI: xecp selftest FAILED on {name}: got {got:?}, want {want:?}"); - } - }; - - // No case starts the list at dword 0 — real controllers occupy the first few. - fn windowed(cells: &[u32; 16]) -> impl Fn(u64) -> Option + '_ { - move |offset: u64| -> Option { - if !offset.is_multiple_of(4) || offset.checked_add(4)? > WINDOW { - return None; - } - Some(cells[(offset / 4) as usize]) - } - } - - check("no list at all", &windowed(&[0; 16]), 0, Ok(None)); - - let mut one = [0u32; 16]; - one[4] = header(2, 0); - check("one capability, not ours", &windowed(&one), 4, Ok(None)); - - let mut third = [0u32; 16]; - third[4] = header(2, 2); - third[6] = header(2, 4); - third[10] = header(CAP_ID_LEGACY, 0); - check("ours, third in the list", &windowed(&third), 4, Ok(Some(40))); - - let mut last = [0u32; 16]; - last[4] = header(2, 11); - last[15] = header(CAP_ID_LEGACY, 0); - check("ours in the last dword there is", &windowed(&last), 4, Ok(Some(60))); - - check( - "xECP points outside the window", - &windowed(&[0; 16]), - 64, - Err(WalkError::OutOfWindow(256)), - ); - - let mut jump = [0u32; 16]; - jump[4] = header(2, 255); - check( - "a link that leaves the window", - &windowed(&jump), - 4, - Err(WalkError::OutOfWindow(16 + 1020)), - ); - - // All-ones is what an unmapped region reads as; it must refuse, not spin. - check( - "a window reading all ones", - &windowed(&[u32::MAX; 16]), - 4, - Err(WalkError::OutOfWindow(16 + 1020)), - ); - - // Conformant but endless; only the iteration cap can end it, the window check never fires. - check( - "an endless chain inside a window that never ends", - &|_| Some(header(2, 1)), - 4, - Err(WalkError::TooMany), - ); - - log!("xHCI: xecp selftest {passed}/{CASES} malformed lists refused"); -} diff --git a/kernel/src/drivers/xhci/mod.rs b/kernel/src/drivers/xhci/mod.rs deleted file mode 100644 index 85b196beadd..00000000000 --- a/kernel/src/drivers/xhci/mod.rs +++ /dev/null @@ -1,1725 +0,0 @@ -mod device; -mod hid; -mod legacy; -pub mod stop; -mod wait; - -use wait::msc; - -/// Everything that waits lives under [`wait`]. -pub use wait::boot::{init, PORT_POLL, PORT_SETTLE_CEILING}; -pub use wait::msc::{storage_flush, storage_read, storage_write}; - -use alloc::vec::Vec; -use core::fmt; -use core::num::NonZeroU8; -use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; - -use crate::arch::barrier; -use crate::mm::Mmio; -use crate::mm::Dma; -use crate::log; -use super::pci::PciDevice; -use crate::sync::{OwedGuard, OwedLock}; -use toyos_xhci::job::{Await, Outcome, Outstanding, Stages, CC_SHORT_PACKET, CC_SUCCESS}; -use toyos_xhci::port::{self as portmachine, GaveUp, Gone, PortState, Reset, Step}; -use toyos_xhci::call::AfterBreak; -use toyos_xhci::recovery::{self, Act, EndpointState, NeedsConfigure, Recovery}; -use toyos_xhci::Protocols; -use toyos_xhci::Portsc; - -use hid::HidDevice; - -const CAP_CAPLENGTH: u64 = 0x00; // u8 -const CAP_HCSPARAMS1: u64 = 0x04; // u32 -const CAP_HCSPARAMS2: u64 = 0x08; // u32 -const CAP_HCCPARAMS1: u64 = 0x10; // u32 -const CAP_DBOFF: u64 = 0x14; // u32 -const CAP_RTSOFF: u64 = 0x18; // u32 - -const OP_USBCMD: u64 = 0x00; -const OP_USBSTS: u64 = 0x04; -const OP_PAGESIZE: u64 = 0x08; -const OP_CRCR: u64 = 0x18; // 64-bit -const OP_DCBAAP: u64 = 0x30; // 64-bit -const OP_CONFIG: u64 = 0x38; -const OP_PORT_BASE: u64 = 0x400; -const PORT_REG_SIZE: u64 = 0x10; - -/// USBCMD's Run/Stop and Host Controller Reset, and USBSTS's HCHalted and -/// Controller Not Ready (xHCI 1.2 §5.4.1, §5.4.2). Named once, because the -/// bring-up and the shutdown drive the same two registers in opposite -/// directions and a second spelling of either is a second answer. -const USBCMD_RS: u32 = 1 << 0; -const USBCMD_HCRST: u32 = 1 << 1; -const USBSTS_HCH: u32 = 1 << 0; -const USBSTS_CNR: u32 = 1 << 11; - -const PORTSC_PP: u32 = 1 << 9; - -/// HCCPARAMS1 bit 3: Port Power Control, which decides PORTSC's PP after reset. -const HCC_PPC: u32 = 1 << 3; - -const IR0_IMAN: u64 = 0x20; // Interrupt Management (IP + IE) -const IR0_IMOD: u64 = 0x24; // Interrupt Moderation -const IR0_ERSTSZ: u64 = 0x28; -const IR0_ERSTBA: u64 = 0x30; // 64-bit -const IR0_ERDP: u64 = 0x38; // 64-bit - -#[repr(C)] -#[derive(Clone, Copy)] -struct Trb { - param: u64, - status: u32, - control: u32, -} - -impl Trb { - const ZERO: Self = Self { param: 0, status: 0, control: 0 }; -} - -const TRB_CYCLE: u32 = 1; - -// TRB type field is bits [15:10] -const fn trb_type(t: u32) -> u32 { t << 10 } - -const TRB_NORMAL: u32 = trb_type(1); -const TRB_SETUP_STAGE: u32 = trb_type(2); -const TRB_DATA_STAGE: u32 = trb_type(3); -const TRB_STATUS_STAGE: u32 = trb_type(4); -const TRB_LINK: u32 = trb_type(6); - -const TRB_ENABLE_SLOT: u32 = trb_type(9); -const TRB_DISABLE_SLOT: u32 = trb_type(10); -const TRB_ADDRESS_DEVICE: u32 = trb_type(11); -const TRB_CONFIGURE_EP: u32 = trb_type(12); -const TRB_EVALUATE_CONTEXT: u32 = trb_type(13); -const TRB_RESET_ENDPOINT: u32 = trb_type(14); -const TRB_STOP_ENDPOINT: u32 = trb_type(15); -const TRB_SET_TR_DEQUEUE: u32 = trb_type(16); -const TRB_RESET_DEVICE: u32 = trb_type(17); - -const EVENT_TRANSFER: u32 = 32; -const EVENT_CMD_COMPLETE: u32 = 33; -const EVENT_PORT_STATUS_CHANGE: u32 = 34; - -const CC_CONTEXT_STATE_ERROR: u32 = 19; -/// Stopped, Stopped - Length Invalid and Stopped - Short Packet (Table 6-90): the transfer events a Stop Endpoint raises for the TRB it stopped inside (§4.6.9). -const CC_STOPPED: core::ops::RangeInclusive = 26..=28; - -/// A completion code, named where xHCI 1.2 §Table 6-90 names it; an unnamed code still keeps its number. -#[derive(Clone, Copy)] -struct Completion(u32); - -impl core::fmt::Display for Completion { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let named = match self.0 { - 1 => "Success", - 2 => "Data Buffer Error", - 3 => "Babble Detected", - 4 => "USB Transaction Error", - 5 => "TRB Error", - 6 => "Stall Error", - 7 => "Resource Error", - 8 => "Bandwidth Error", - 9 => "No Slot Available", - 10 => "Invalid Stream Type", - 11 => "Slot Not Enabled", - 12 => "Endpoint Not Enabled", - 13 => "Short Packet", - 14 => "Ring Underrun", - 15 => "Ring Overrun", - 16 => "VF Event Ring Full", - 17 => "Parameter Error", - 18 => "Bandwidth Overrun", - 19 => "Context State Error", - 20 => "No Ping Response", - 21 => "Event Ring Full", - 22 => "Incompatible Device", - 23 => "Missed Service Error", - 24 => "Command Ring Stopped", - 25 => "Command Aborted", - 26 => "Stopped", - 27 => "Stopped - Length Invalid", - 28 => "Stopped - Short Packet", - 29 => "Max Exit Latency Too Large", - 31 => "Isoch Buffer Overrun", - 32 => "Event Lost", - 33 => "Undefined Error", - 34 => "Invalid Stream ID", - 35 => "Secondary Bandwidth Error", - 36 => "Split Transaction Error", - _ => return write!(f, "code {}", self.0), - }; - write!(f, "code {} ({named})", self.0) - } -} - -/// What the controller's answer to the one outstanding operation is for; no variant may be done by spinning inside a scheduler pass. -enum What { - /// Disable Slot, and what stops being reachable once it completes. - SlotGone { slot: u8, then: AfterSlot }, - /// One step of a HID endpoint's recovery; `seq` carries where it is, `issued` names the command for failure lines. - Recovering { slot_id: u8, seq: Recovery, issued: &'static str }, - /// Enable Slot for a port that finished reset; there is no device until the controller answers. - SlotWanted { - port_idx: u8, - speed: u8, - usb_speed: toyos_abi::inventory::UsbSpeed, - packet: u16, - seq: toyos_xhci::enumerate::Enumeration, - }, - /// One act of a device's enumeration. - Enumerating(device::Enumerating), -} - -/// What the controller said about one outstanding operation, for a refusal line. -struct Answer(Outcome); - -impl core::fmt::Display for Answer { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self.0 { - Outcome::Command { code, .. } | Outcome::Transfer { code, .. } => { - write!(f, "{}", Completion(code)) - } - Outcome::Silent => { - write!(f, "no answer in {} ms", USB_TIMEOUT_NS / 1_000_000) - } - } - } -} - -/// Which device a line is about: the controller and the slot on it — a slot id alone is ambiguous, since a machine has more than one controller. -#[derive(Clone, Copy)] -struct Slot { - bus: u8, - dev: u8, - func: u8, - id: u8, -} - -impl core::fmt::Display for Slot { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{:02x}:{:02x}.{} slot {}", self.bus, self.dev, self.func, self.id) - } -} - -/// Why a slot was given back, which is what decides what goes with it. -enum AfterSlot { - /// A port's device has left the bus; the port may be enumerated again. - Teardown(u8), - /// Given up on while still plugged in; the port stays marked attached — see [`XhciController::let_go`] and [`XhciController::give_back_offline_slots`]. - LetGo, - /// Enumeration ended in refusal with the device still plugged in; the port stays attached so it is not re-enumerated every debounce. - Refused, - /// A disk refused for a reason a later look may not find — not ready, or no pool block free — while a disk on this controller is held for its device: the refused one may be that device, back early or slow, so its block goes back and its port is enumerated again rather than left refused until a replug. - Again(u8), -} - -/// The earlier of two instants something wants to be looked at again. -fn earliest(a: Option, b: Option) -> Option { - match (a, b) { - (Some(a), Some(b)) => Some(a.min(b)), - (at, None) | (None, at) => at, - } -} - -/// Where a control transfer's completions come from: addresses, not a count — [`Await::Transfer`] matches by TRB address. -#[derive(Clone, Copy)] -struct ControlTrbs { - /// The Data Stage TRB, present only for a transfer that carries data. - data: Option, - /// The Status Stage TRB, every control transfer's device verdict. - status: u64, -} - -impl ControlTrbs { - /// What the driver waits for, as [`Outstanding::submit`] wants it. - fn awaits(self, slot: u8) -> (Await, Stages) { - let on = |trb| Await::Transfer { slot, dci: 1, trb }; - match self.data { - Some(data) => (on(data), Stages::DataThenStatus(on(self.status))), - None => (on(self.status), Stages::One), - } - } -} - -/// Put one control transfer's TRBs on an EP0 ring, and say where each completion will come from. -fn enqueue_control( - ring: &mut TrbRing, - bm_request_type: u8, - b_request: u8, - w_value: u16, - w_index: u16, - data_buf: Option, - data_len: u16, -) -> ControlTrbs { - let is_in = (bm_request_type & 0x80) != 0; - let has_data = data_len > 0 && data_buf.is_some(); - let trt = if !has_data { 0u32 } else if is_in { 3 } else { 2 }; - - let mut setup = Trb::ZERO; - setup.param = setup_packet(bm_request_type, b_request, w_value, w_index, data_len); - setup.status = 8; - setup.control = TRB_SETUP_STAGE | (1 << 6) | (trt << 16); - ring.enqueue(setup); - - let data_at = data_buf.filter(|_| has_data).map(|buf| { - let mut data = Trb::ZERO; - data.param = buf; - data.status = data_len as u32; - let dir = if is_in { 1u32 << 16 } else { 0 }; - // ISP and IOC both required: without IOC the data stage produces no event, without ISP a short answer is not reported. - data.control = TRB_DATA_STAGE | dir | (1 << 2) | (1 << 5); - ring.enqueue(data) - }); - - let mut status = Trb::ZERO; - let status_dir = if has_data && is_in { 0 } else { 1u32 << 16 }; - status.control = TRB_STATUS_STAGE | (1 << 5) | status_dir; - ControlTrbs { data: data_at, status: ring.enqueue(status) } -} - -/// The line for an endpoint no sequence of commands takes back to Running. -fn log_unrecoverable(slot: Slot, dci: u8, state: EndpointState) { - log!("xHCI: {slot} endpoint {dci} is {state}; no command this driver issues is defined \ - for an endpoint in that state"); -} - -/// How many *consecutive* transfer failures let an interrupt endpoint's device go; a delivered report clears the count. -const MAX_HID_FAILURES: u8 = 8; - -/// How long the driver waits on any one command or transfer. -/// -/// Not proof of a dead device: under TCG the CPU's counter tracks host wall-clock, so a live device on an oversubscribed host can breach this once; only three breaches in one command are treated as a device fact. -/// -/// USB_TIMEOUT_NS and `block::OPERATION` are the same 2 s; the two must stay equal or the budget math (`Scsi::Budget` → `BlockError::BudgetExpired`) is wrong. -const USB_TIMEOUT_NS: u64 = 2_000_000_000; - -/// What one disk call may spin for once its transport has broken, part by part: the wait that broke and the commands sent again after a rung that took, then each rung of `toyos_xhci::ladder` on a bound of its own, which `toyos_xhci::call` keeps the parts before it out of. -pub(crate) const AFTER_BREAK: toyos_xhci::call::Bounds = toyos_xhci::call::AFTER_BREAK; - -// The wait that broke is given what any wait is. -const _: () = assert!(AFTER_BREAK.wait == USB_TIMEOUT_NS); - -/// Everything one disk call may spin for from the start of the wait its transport broke on. -const CALL_AFTER_BREAK: crate::time::Budget = crate::time::Budget::of( - crate::time::Duration::from_nanos(AFTER_BREAK.whole()), - "every wait is clipped to where the rungs still ahead of it begin, so the last rung runs whatever was spent before it and the call ends here", -); - -// A bind spins in the tick's pass with interrupts off, so one that outlasted -// this tripwire would panic another CPU over a device. -const _: () = assert!(CALL_AFTER_BREAK.nanos() < crate::time::DEAF_CPU.nanos()); - - -// A disk whose device left under the port rung's reset is waited for no longer than the rungs from that reset on were given to spend on it. -const _: () = assert!( - toyos_xhci::identity::RETURN_WINDOW <= AFTER_BREAK.port_reset + AFTER_BREAK.offline -); - -/// When a wait started now would give up; unbounded before `clock::init`, since `nanos_since_boot` stays 0. -fn deadline() -> u64 { - crate::clock::nanos_since_boot() + USB_TIMEOUT_NS -} - -/// The boot-time connect settle reads the same interval the per-port machine uses. -use portmachine::DEBOUNCE_NS as PORT_DEBOUNCE_NS; - -/// One bit per root-hub port; four words cover every MaxPorts a byte can express. -type PortMask = [u64; 4]; - -/// When some controller's port state machine must be stepped again, or 0 for none; neither reader may take [`XHCI`]. -/// -/// A CPU with nothing else to run must not sleep while this is set — nothing else would wake it for deferred port work. -static PORT_WORK_AT: AtomicU64 = AtomicU64::new(0); - -/// Whether a CPU with nothing to run must stay awake for [`PORT_WORK_AT`]. -pub fn port_work_pending() -> bool { - PORT_WORK_AT.load(Ordering::Relaxed) != 0 -} - -/// Read every root-hub port again now, and step whatever has changed since. -/// -/// For a caller with a reason of its own to keep looking: the boot scan is not a census, and [`poll_if_pending`] stops looking once it stores zero. -pub fn recheck_ports() { - let mut wake_at: Option = None; - for ctrl in XHCI.lock().iter_mut() { - ctrl.ports_dirty = true; - if let Some(at) = ctrl.poll() { - wake_at = Some(wake_at.map_or(at, |w: u64| w.min(at))); - } - } - PORT_WORK_AT.store(wake_at.unwrap_or(0), Ordering::Relaxed); -} - -const RING_SIZE: usize = 256; // TRBs per ring (one page = 256 * 16) - -/// Clear `len` bytes of `dma` at `off`, and answer with the region that was cleared. -pub(super) fn zero_dma<'pool>(dma: Dma<'pool>, off: usize, len: usize) -> Dma<'pool> { - let region = dma.subview(off, len); - region.zero(); - region -} - -/// Point the DCBAA's `slot` entry at `phys`, or clear it with zero — the DCBAA's one writer. Slot 0 is the scratchpad array pointer, not a device context, which is why `init_one` is also a caller of `write_dcbaa`. -pub(super) fn write_dcbaa(dma: Dma<'_>, slot: usize, phys: u64) { - // Volatile: the controller reads this array on any slot id, so the store may not be reordered against the command that follows. - dma.write::(OFF_DCBAA + slot * core::mem::size_of::(), phys) -} - -/// Event Ring Segment Table entry (16 bytes). -#[repr(C)] -#[derive(Clone, Copy)] -struct ErstEntry { - ring_base: u64, - ring_size: u32, - _reserved: u32, -} - -/// `buf` and not a raw pointer: a [`Dma`] view carries the length that [`TrbRing::put`] checks each TRB against. -#[derive(Clone, Copy)] -struct TrbRing { - buf: Dma<'static>, - base_phys: u64, - tail: u16, - cycle: bool, -} - -impl TrbRing { - /// A fresh ring, zeroed with the wrap link TRB at the last slot; also the recovery primitive, paired with a Set TR Dequeue Pointer naming [`Self::dequeue`]. - fn init(buf: Dma<'static>) -> Self { - assert!(buf.size() >= RING_SIZE * core::mem::size_of::()); - zero_dma(buf, 0, buf.size()); - let ring = Self { buf, base_phys: buf.device_addr(), tail: 0, cycle: true }; - let mut link = Trb::ZERO; - link.param = buf.device_addr(); - link.control = TRB_LINK | (1 << 1); // TC (Toggle Cycle) - ring.put(RING_SIZE - 1, link); - ring - } - - /// One TRB at ring index `at` — the ring's one writer. - /// - /// The control word last, behind a barrier: it carries the Cycle bit, and a - /// controller running this ring owns the TRB the moment the bit matches its - /// own (xHCI 1.2 §4.9.2), so the body must already be there. - fn put(&self, at: usize, trb: Trb) { - let off = at * core::mem::size_of::(); - self.buf.write(off + core::mem::offset_of!(Trb, param), trb.param); - self.buf.write(off + core::mem::offset_of!(Trb, status), trb.status); - barrier::dma_wmb(); - self.buf.write(off + core::mem::offset_of!(Trb, control), trb.control); - } - - /// Where the controller should resume, with the cycle state it must expect; bit 0 carries the cycle since a TRB address is 16-byte aligned. - fn dequeue(&self) -> u64 { - (self.base_phys + (self.tail as u64) * 16) | (self.cycle as u64) - } - - /// Put `trb` on the ring and answer with where it landed — the only name a Command Completion or Transfer Event carries (xHCI 1.2 §6.4.2.1–2). - fn enqueue(&mut self, mut trb: Trb) -> u64 { - if self.cycle { - trb.control |= TRB_CYCLE; - } else { - trb.control &= !TRB_CYCLE; - } - let at = self.base_phys + (self.tail as u64) * 16; - self.put(self.tail as usize, trb); - self.tail += 1; - - if self.tail as usize >= RING_SIZE - 1 { - let mut link = Trb::ZERO; - link.param = self.base_phys; - link.control = TRB_LINK | (1 << 1); // TC (Toggle Cycle) - if self.cycle { link.control |= TRB_CYCLE; } - self.put(self.tail as usize, link); - self.tail = 0; - self.cycle = !self.cycle; - } - at - } -} - -/// The granularity every structure below is placed at, asserted against the controller's own PAGESIZE rather than assumed. -const PAGE: usize = 0x1000; - -// The pool's fixed head: one of each, since enumeration is serial — see `device::init_device`. -#[allow(clippy::erasing_op)] -const OFF_DCBAA: usize = 0 * PAGE; // (max_slots + 1) * 8, 2 KiB at most -#[allow(clippy::identity_op)] -const OFF_CMD_RING: usize = 1 * PAGE; -const OFF_ERST: usize = 2 * PAGE; -const OFF_EVT_RING: usize = 3 * PAGE; -const OFF_INPUT_CTX: usize = 4 * PAGE; // 33 contexts, so 2112 B at ctx_size 64 -const OFF_DATA_BUF: usize = 5 * PAGE; -const SHARED_SIZE: usize = 6 * PAGE; - -// One block per device; sharing any of these four regions between two devices is a silent data race. -/// The Device Context Index of the default control pipe, 1 for every device (xHCI 1.2 §4.5.1). -const EP0_DCI: u8 = 1; - -const DEV_INT_RING: usize = 0; // 256 TRBs, exactly one page -const DEV_EP0_RING: usize = PAGE; // likewise -const DEV_OUT_CTX: usize = 2 * PAGE; // 32 contexts, 2 KiB at ctx_size 64 -const DEV_REPORT: usize = 2 * PAGE + 0x800; // 8 B, the largest boot report -const DEV_STRIDE: usize = 3 * PAGE; - -// Separate from the device block: folding this into DEV_STRIDE would cost every device 64 KiB it never touches. -const MSC_IN_RING: usize = 0; -const MSC_OUT_RING: usize = PAGE; -const MSC_CBW: usize = 2 * PAGE; // 31 B -const MSC_CSW: usize = 2 * PAGE + 0x40; // 13 B -const MSC_SCRATCH: usize = 2 * PAGE + 0x80; // INQUIRY, READ CAPACITY, sense -const MSC_SCRATCH_LEN: usize = 64; -/// The input context of the commands a rung of the ladder issues for this disk. -const MSC_INPUT_CTX: usize = 3 * PAGE; -/// The bulk data buffer, placed so it cannot cross a 64 KiB boundary — the one placement rule an xHCI Normal TRB's buffer has. -const MSC_DATA: usize = 8 * PAGE; -const MSC_DATA_LEN: usize = 8 * PAGE; -const MSC_STRIDE: usize = 16 * PAGE; - -/// Mass-storage devices the pool has blocks for; a third stick is refused rather than served from somebody else's block. -const MSC_BLOCKS: usize = 2; - -/// The largest run of 4 KiB blocks one SCSI command moves; every caller-facing loop batches to it. -const MSC_MAX_BLOCKS: u32 = (MSC_DATA_LEN / 4096) as u32; - -/// Device blocks to size the pool for before the controller's slot count is consulted; without this floor a scratchpad demand near a 2 MiB boundary can leave zero room for devices. -const MIN_DEVICE_BLOCKS: usize = 8; - -/// Where each structure sits in the pool, derived from what the controller reported. -#[derive(Clone, Copy)] -struct Layout { - scratch_array: usize, - scratch_buffers: usize, - scratch_count: usize, - msc_base: usize, - dev_base: usize, - /// Device blocks the pool holds, also the MaxSlotsEn written to CONFIG. - dev_blocks: usize, - pool_size: usize, -} - -impl Layout { - /// `max_scratchpad` and `max_slots` come straight off HCSPARAMS, bounded so the plain `align_2m` below cannot overflow — unlike every other caller of a size from outside the kernel, which needs `align_2m_checked`. - fn new(max_scratchpad: usize, max_slots: u8) -> Self { - let scratch_array = SHARED_SIZE; - let array_bytes = (max_scratchpad * 8 + PAGE - 1) & !(PAGE - 1); - let scratch_buffers = scratch_array + array_bytes; - // Ahead of the device array, not behind it, so the device array still absorbs the pool's slack. - let msc_base = - (scratch_buffers + max_scratchpad * PAGE + MSC_STRIDE - 1) & !(MSC_STRIDE - 1); - let dev_base = msc_base + MSC_BLOCKS * MSC_STRIDE; - - // DmaPool hands out whole 2 MiB pages; the floor decides how many pages, the slack decides how many device blocks. - let pool_size = crate::mm::align_2m(dev_base + MIN_DEVICE_BLOCKS * DEV_STRIDE); - let dev_blocks = ((pool_size - dev_base) / DEV_STRIDE) - .min(max_slots as usize); - - Self { - scratch_array, - scratch_buffers, - scratch_count: max_scratchpad, - msc_base, - dev_base, - dev_blocks, - pool_size, - } - } - - /// The block for a 1-based slot id, or `None` when the pool has no room; `slot_id` is untrusted, from the controller's own answer. - fn device(&self, slot_id: u8) -> Option { - let index = usize::from(slot_id.checked_sub(1)?); - if index >= self.dev_blocks { - return None; - } - Some(self.dev_base + index * DEV_STRIDE) - } - - /// The `index`-th mass-storage block; private since only [`XhciController::claim_msc_block`] decides what `index` is. - fn msc(&self, index: usize) -> usize { - self.msc_base + index * MSC_STRIDE - } -} - -/// One mass-storage pool block, and whatever is holding it. -/// -/// One struct and not two separate flags: a device refused between Configure Endpoint and READ CAPACITY would otherwise hold a block nothing names. -#[derive(Clone, Copy)] -struct MscBlock { - /// The port whose device claimed this block, `None` while free; claimed before Configure Endpoint, given back only after the slot is disabled. - port: Option, - /// The disk, once `bring_up` produced one; `None` for a refused device, which is why `port` is what says taken. - disk: Option, -} - -impl MscBlock { - const FREE: Self = Self { port: None, disk: None }; -} - -/// A disk this controller brought up, and the number the machine knows it by. -#[derive(Clone, Copy)] -struct Disk { - index: usize, - dev: msc::MscDevice, -} - -/// A disk whose device left its port under a reset of this driver's, held under -/// its number for the same device to come back (`toyos_xhci::identity`). -#[derive(Clone, Copy)] -struct Awaited { - index: usize, - identity: toyos_xhci::identity::Identity, - /// The port it left, which its block stays claimed by until that port's - /// teardown. - port_idx: u8, - returns_by: u64, - /// It left owing a flush (`msc::MscDevice::owes_a_flush`). - owed_flush: bool, - /// The disk's loss count it handed on, this departure counted - /// (`msc::MscDevice::losses_left`). - losses: u64, -} - -/// Where the machine's `index`-th disk is, for an operation that found it -/// answering nothing. -pub(super) enum Whereabouts { - /// Bound, on some controller's pool block. - Here, - /// Held for its device to come back. - Awaited, - /// Neither: unplugged, never bound, or not back in time. - Gone, -} - -/// Where the machine's `index`-th disk is, forgetting it if its window has -/// passed; `None` while the controller lock is taken, which a bind may hold for -/// seconds. -/// -/// Every port is marked to be read again, so whoever steps the ports next reads -/// them all. -pub(super) fn look_for(index: usize) -> Option { - let now = crate::clock::nanos_since_boot(); - let mut guard = XHCI.try_lock()?; - for ctrl in guard.iter_mut() { - ctrl.ports_dirty = true; - } - for ctrl in guard.iter_mut() { - if ctrl.msc.iter().any(|block| block.disk.is_some_and(|d| d.index == index)) { - return Some(Whereabouts::Here); - } - ctrl.forget_the_unreturned(now); - if ctrl.awaited.iter().any(|a| a.index == index) { - return Some(Whereabouts::Awaited); - } - } - Some(Whereabouts::Gone) -} - -/// Have the ports stepped now by a CPU that reaches a scheduler pass, for a -/// caller that waits for a device on a CPU it holds with `IF` clear: the -/// interrupt a connect raises may be this CPU's, and it takes none. `kick` -/// wakes every other CPU, since a halted one has stopped its own timer; the -/// kick is refused only before `irqchip::init`, and no other CPU has been started -/// before it. -pub(super) fn ports_wanted(kick: bool) { - PORT_WORK_AT.store(crate::clock::nanos_since_boot().max(1), Ordering::Relaxed); - if !kick { - return; - } - let me = crate::arch::percpu::cpu_id(); - for cpu in 0..crate::smp::cpu_count() { - if cpu != me { - crate::arch::irqchip::kick_cpu(cpu); - } - } -} - -/// How many disks this machine has bound since boot, and the number the next bind hands out. -/// -/// A counter and not a position, and never reused: `usb_storage::handle` indexes by it and a mount holds it for the disk's whole life. -static DISKS_BOUND: AtomicUsize = AtomicUsize::new(0); - -fn setup_packet(bm_request_type: u8, b_request: u8, w_value: u16, w_index: u16, w_length: u16) -> u64 { - (bm_request_type as u64) - | ((b_request as u64) << 8) - | ((w_value as u64) << 16) - | ((w_index as u64) << 32) - | ((w_length as u64) << 48) -} - -/// `Send` is derived, not asserted: every field is `Send` on its own, so no `unsafe impl` is owed. -pub struct XhciController { - /// The function this controller is, so a log line can name which controller it means. - pci: PciDevice, - - op_base: Mmio, - db_base: Mmio, - rt_base: Mmio, - - /// HCSPARAMS1's MaxPorts: every port register this controller has. - max_ports: u8, - - /// When this controller's ports were powered, where [`PORT_DEBOUNCE_NS`] is measured from; kept per controller so two controllers' debounces overlap rather than add. - powered_at: u64, - - context_size: usize, // 32 or 64 - layout: Layout, - - /// This controller's DMA, and this controller's only — a shared pool would collide two controllers' DCBAAs and command rings at the same address. - /// - /// Leaked late, only once `init_one`'s last refusal is behind it, so a declined controller still gives its pages back. - /// - /// A `DmaPool` field here plus a `TrbRing` borrowing it would be a self-reference, which is what forces the leaked `Dma<'static>` instead. - pool: Dma<'static>, - - cmd_ring: TrbRing, - - /// The event ring as a region, not a pointer, so `next_event`'s read is bounded against the page. - event_ring: Dma<'static>, - event_head: u16, - event_phase: bool, - - devices: Vec, - - /// This controller's mass-storage pool blocks and their disks. - /// - /// Claimed before Configure Endpoint, before the disk is known, since keying off a count of *bound* disks would hand a live endpoint's memory to the next one. - msc: [MscBlock; MSC_BLOCKS], - - /// Disks whose device left its port under this driver's reset, until the - /// same device binds or their window passes. - awaited: Vec, - - /// This controller's root-hub ports, sized from HCSPARAMS1 rather than fixed at 255. - ports: Vec, - - /// What each port register speaks, from the controller's Supported Protocol capabilities. - protocols: Protocols, - - /// A Port Status Change Event arrived and the ports have not been read since; consumed by [`Self::poll`]. - ports_dirty: bool, - - /// The one operation this controller has been given and has not answered. - /// - /// Submitted and left, not spun on: a scheduler pass may not block to [`USB_TIMEOUT_NS`] against a device with nothing to answer. - outstanding: Outstanding, - - /// What the disk call now inside this controller may still spend, once its transport has broken; closed between calls. - after_break: AfterBreak, - - /// When the bulk wait now running, or the last one to run, began: where a break opens [`Self::after_break`] from. - bulk_began: u64, - - /// The last Stopped transfer event on an endpoint no HID device owns, as (slot, dci, completion code, TRB Transfer Length): what a Stop Endpoint found a disk's transfer at, for the quiesce that issued it to read. - stopped: Option<(u8, u8, u32, u32)>, -} - -impl XhciController { - pub(super) fn dma(&self) -> Dma<'static> { - self.pool - } - - fn write_dcbaa(&self, slot: usize, phys: u64) { - write_dcbaa(self.dma(), slot, phys); - } - - /// This controller's `slot_id` as a [`Slot`]; never construct one from a bare slot id, since the controller is half the identity. - fn slot(&self, id: u8) -> Slot { - Slot { bus: self.pci.bus, dev: self.pci.dev, func: self.pci.func, id } - } - - /// Every read of a port register in this driver, so no two callers can disagree about it. - fn read_portsc(&self, port_idx: u8) -> Portsc { - Portsc::from_raw(self.read_portsc_raw(port_idx)) - } - - fn read_portsc_raw(&self, port_idx: u8) -> u32 { - self.op_base.read_u32(OP_PORT_BASE + port_idx as u64 * PORT_REG_SIZE) - } - - /// Every write of a port register; takes a typed [`toyos_xhci::portsc::Write`], which offers no way to set PED, so disabling a port the driver is enabling is unreachable rather than asserted against. - fn write_portsc(&mut self, port_idx: u8, write: toyos_xhci::portsc::Write) { - let value = write.raw(); - self.op_base.write_u32(OP_PORT_BASE + port_idx as u64 * PORT_REG_SIZE, value); - } - - /// The port a slot's device is on, or `None` for a slot mid-enumeration — `device::finish` is what gives a port its slot. - fn port_of_slot(&self, slot: u8) -> Option { - self.ports - .iter() - .position(|p| p.slot().map(NonZeroU8::get) == Some(slot)) - .map(|at| at as u8) - } - - fn connected_ports(&self) -> PortMask { - let mut mask = [0u64; 4]; - for p in 0..self.max_ports { - if self.read_portsc(p).connected() { - mask[p as usize / 64] |= 1 << (p % 64); - } - } - mask - } - - /// Take a free mass-storage pool block for `port_idx`, as its index and byte offset; the only way to obtain one, so nothing is unrecorded. - fn claim_msc_block(&mut self, port_idx: u8) -> Option<(usize, usize)> { - let index = self.msc.iter().position(|block| block.port.is_none())?; - self.msc[index].port = Some(port_idx); - Some((index, self.layout.msc(index))) - } - - /// How many blocks are spoken for. - fn msc_blocks_taken(&self) -> usize { - self.msc.iter().filter(|block| block.port.is_some()).count() - } - - /// Put a command on the ring and ring the doorbell, answering with the address the completion will name it by. - fn submit_command(&mut self, trb: Trb) -> u64 { - let at = self.cmd_ring.enqueue(trb); - // An `Mmio` write: ordered after the TRB it announces. - self.db_base.write_u32(0, 0); - at - } - - /// One event, or `None` while the controller has not published the next; every reader goes through here since the ring is one shared queue. - fn next_event(&mut self) -> Option { - // Volatile so the poll observes the Cycle bit flipping; racing the controller by design (xHCI 1.2 §4.9.2). - // The control word alone first: the rest of the TRB is the controller's - // until the Cycle bit it carries says otherwise. - let at = self.event_head as usize * core::mem::size_of::(); - let control: u32 = self.event_ring.read(at + core::mem::offset_of!(Trb, control)); - if ((control & 1) != 0) != self.event_phase { - return None; - } - barrier::dma_rmb(); - let event: Trb = self.event_ring.read(at); - self.advance_event_ring(); - Some(event) - } - - /// Give an event to the device it names; an interrupt completion dropped here leaves the device's ring empty for the life of the boot. - /// - /// A code other than Success or Short Packet is recorded rather than dropped, so [`Self::recover_endpoints`] can act on it. - fn dispatch_event(&mut self, event: Trb) { - let trb_type = (event.control >> 10) & 0x3F; - let code = (event.status >> 24) & 0xFF; - let slot = ((event.control >> 24) & 0xFF) as u8; - - // The outstanding operation first: both event kinds name their TRB in the first two dwords, low 4 bits reserved and masked out (§6.4.2.1–2). - let answers = match trb_type { - EVENT_CMD_COMPLETE => Some((Await::Command { trb: event.param & !0xF }, slot as u32)), - EVENT_TRANSFER => Some(( - Await::Transfer { - slot, - dci: ((event.control >> 16) & 0x1F) as u8, - trb: event.param & !0xF, - }, - event.status & 0x00FF_FFFF, - )), - _ => None, - }; - if answers.is_some_and(|(on, param)| self.outstanding.answered(on, code, param)) { - return; - } - // The port id the event carries is not read — the register is what says what a port is, and the event is only a reason to look. - if trb_type == EVENT_PORT_STATUS_CHANGE { - self.ports_dirty = true; - return; - } - if trb_type != EVENT_TRANSFER { - return; - } - // Skip: a mid-recovery endpoint's ring is about to be rebuilt, and requeueing now would land a TRB where the dequeue pointer is not. - if matches!(self.outstanding.what(), Some(What::Recovering { slot_id, .. }) if *slot_id == slot) - { - return; - } - let Some(at) = self.devices.iter().position(|d| d.slot_id == slot) else { - if CC_STOPPED.contains(&code) { - let dci = ((event.control >> 16) & 0x1F) as u8; - self.stopped = Some((slot, dci, code, event.status & 0x00FF_FFFF)); - } - return; - }; - let dev = &mut self.devices[at]; - if code == CC_SUCCESS || code == CC_SHORT_PACKET { - dev.failures = 0; - dev.dispatch_report(event.status & 0x00FF_FFFF); - dev.requeue(&self.db_base); - return; - } - dev.broke_with = Some(code); - } - - /// Start recovering one HID endpoint a completion code broke, if the controller is free. - /// - /// Separate from `dispatch_event`, which runs inside another caller's wait: a recovery issued there would consume that caller's completion. - /// - /// One recovery at a time and never more: `Self::outstanding` is one slot, so a second device's recovery is owed until the first is answered. - fn recover_endpoints(&mut self) { - if self.outstanding.busy() { - return; - } - let Some((slot_id, code)) = - self.devices.iter().find_map(|d| Some((d.slot_id, d.broke_with?))) - else { - return; - }; - self.recover_hid(slot_id, code); - } - - /// One HID device's interrupt endpoint, on its way back to Running — or off the bus. - fn recover_hid(&mut self, slot_id: u8, code: u32) { - let Some(at) = self.devices.iter().position(|d| d.slot_id == slot_id) else { - return; - }; - // Stays on the list: a device not on it is one a port teardown cannot find. - let dev = &mut self.devices[at]; - dev.broke_with = None; - let kind = dev.kind(); - let (ep_addr, dci, port_idx, block) = - (dev.ep_addr, dev.int_ep_dci, dev.port_idx, dev.block); - - // Disconnect wins the race: a transaction-error code from a pulled device is indistinguishable from a bad cable, only the port register tells them apart. - let slot = self.slot(slot_id); - let portsc = self.read_portsc(port_idx); - if !portsc.holds() { - log!("xHCI: USB {kind} on {slot}: interrupt endpoint {ep_addr:#04x} \ - completed with {} as its port went away; leaving it to the disconnect", - Completion(code)); - return; - } - - let dev = &mut self.devices[at]; - dev.failures += 1; - let failures = dev.failures; - log!("xHCI: USB {kind} on {slot}: interrupt endpoint {ep_addr:#04x} (dci {dci}) \ - completed with {}; failure {failures} of {MAX_HID_FAILURES}", - Completion(code)); - - if failures >= MAX_HID_FAILURES { - self.let_go(at, format_args!("it has failed {MAX_HID_FAILURES} transfers in a row")); - return; - } - let state = self.endpoint_state(block, dci); - log!("xHCI: {slot} endpoint {dci} is {state}, recovering"); - match Recovery::begin(state) { - Ok((seq, act)) => self.step_recovery(slot_id, seq, act), - Err(NeedsConfigure(state)) => { - log_unrecoverable(slot, dci, state); - self.let_go(at, format_args!("endpoint {ep_addr:#04x} could not be restarted")); - } - } - } - - /// Perform one act of a HID endpoint's recovery; nothing here waits — the completion arrives through the poll's own drain. - fn step_recovery(&mut self, slot_id: u8, seq: Recovery, act: Act) { - let Some(at) = self.devices.iter().position(|d| d.slot_id == slot_id) else { - return; - }; - let slot = self.slot(slot_id); - let dev = &mut self.devices[at]; - let (dci, ep_addr, ring_at) = (dev.int_ep_dci, dev.ep_addr, dev.block + DEV_INT_RING); - match act { - Act::Running => { - dev.requeue(&self.db_base); - log!("xHCI: {slot} endpoint {dci} is delivering again"); - } - Act::Command(cmd) => { - // Copied out and back rather than borrowed: `recovery_trb` reads the pool through `self` in this window. - let mut ring = dev.int_ring; - let trb = self.recovery_trb(cmd, slot_id, dci, &mut ring, ring_at); - self.devices[at].int_ring = ring; - let on = Await::Command { trb: self.submit_command(trb) }; - let what = What::Recovering { slot_id, seq, issued: cmd.name() }; - self.outstanding.submit(what, on, Stages::One, deadline()); - } - Act::ClearHalt => { - let trbs = enqueue_control( - &mut self.devices[at].ep0_ring, 0x02, 0x01, 0, ep_addr as u16, None, 0, - ); - self.ring_doorbell(slot_id, 1); - let (on, stages) = trbs.awaits(slot_id); - let what = - What::Recovering { slot_id, seq, issued: "CLEAR_FEATURE(ENDPOINT_HALT)" }; - self.outstanding.submit(what, on, stages, deadline()); - } - } - } - - /// The controller answered a recovery step; ask the sequence what is owed next, or let the device go. - fn recovery_stepped( - &mut self, - slot_id: u8, - mut seq: Recovery, - issued: &str, - outcome: Outcome, - ) { - if outcome.succeeded() { - let act = seq.completed(); - self.step_recovery(slot_id, seq, act); - return; - } - log!("xHCI: {}: {issued} failed: {}", self.slot(slot_id), Answer(outcome)); - if let Some(at) = self.devices.iter().position(|d| d.slot_id == slot_id) { - self.let_go(at, format_args!("its interrupt endpoint could not be restarted")); - } - } - - /// Drop a recovery outstanding on a port whose device has gone — a transfer error there belongs to the disconnect, not the endpoint. - fn cancel_recovery_on(&mut self, port_idx: u8) { - let Some(What::Recovering { slot_id, .. }) = self.outstanding.what() else { - return; - }; - let slot_id = *slot_id; - if !self.devices.iter().any(|d| d.slot_id == slot_id && d.port_idx == port_idx) { - return; - } - self.outstanding.cancel(); - log!("xHCI: {}'s endpoint recovery is abandoned; its port has gone", self.slot(slot_id)); - } - - /// Everything a HID device the driver has given up on leaves behind. - /// - /// Unlike [`Self::teardown_port`], the port stays marked attached: clearing it would enumerate the same endpoint again every debounce. - /// - /// The port's slot is this device's: one root-hub port carries one device here, and `parse_config` gives that device one function. - fn let_go(&mut self, at: usize, why: core::fmt::Arguments) { - let mut dev = self.devices.remove(at); - log!("xHCI: USB {} on {} is being let go — {why}. Unplug it and plug it in again.", - dev.kind(), self.slot(dev.slot_id)); - dev.unbind(); - if let Some(slot) = self.ports[dev.port_idx as usize].take_slot() { - self.submit_disable_slot(slot.get(), AfterSlot::LetGo); - } - } - - /// The slot of every disk the transport took offline, back to the controller. - /// - /// From the poll and never from the disk operation that took it offline: that operation runs beside whatever is outstanding, and [`Self::submit_disable_slot`] is one operation at a time. The port's slot is this device's, as in [`Self::let_go`]. - /// - /// The port stays attached and the pool block stays the port's until the unplug: the device on it answered nothing after a port reset, and enumerating it again asks the same question. - fn give_back_offline_slots(&mut self) { - for at in 0..MSC_BLOCKS { - if self.outstanding.busy() { - return; - } - let Some(disk) = self.msc[at].disk.as_mut() else { continue }; - let Some(port_idx) = disk.dev.take_slot_owed() else { continue }; - if let Some(slot) = self.ports[port_idx as usize].take_slot() { - self.submit_disable_slot(slot.get(), AfterSlot::LetGo); - } - } - } - - /// The command `cmd` names against (`slot_id`, `dci`), rebuilding the ring where the command is Set TR Dequeue. - fn recovery_trb( - &self, - cmd: recovery::Command, - slot_id: u8, - dci: u8, - ring: &mut TrbRing, - ring_at: usize, - ) -> Trb { - let mut trb = Trb::ZERO; - let kind = match cmd { - recovery::Command::ResetEndpoint => TRB_RESET_ENDPOINT, - recovery::Command::StopEndpoint => TRB_STOP_ENDPOINT, - recovery::Command::SetDequeue => { - *ring = TrbRing::init(self.dma().subview(ring_at, PAGE)); - trb.param = ring.dequeue(); - TRB_SET_TR_DEQUEUE - } - }; - trb.control = kind | ((slot_id as u32) << 24) | ((dci as u32) << 16); - trb - } - - /// Clear whatever change flags one port is holding, using the value the caller already read — a fresh read here could clear a flag never looked at. - fn acknowledge_port_change(&mut self, port_idx: u8, portsc: Portsc) { - if portsc.any_change() { - self.write_portsc(port_idx, portsc.neutral().acknowledging(portsc)); - } - } - - /// Read a port and clear whatever change flags it is holding. - fn acknowledge_port_read(&mut self, port_idx: u8) { - let portsc = self.read_portsc(port_idx); - self.acknowledge_port_change(port_idx, portsc); - } - - /// The same for every port this controller has. - fn acknowledge_port_changes(&mut self) { - for p in 0..self.max_ports { - self.acknowledge_port_read(p); - } - } - - /// Step every port that is not where the driver left it, and say when it wants to be looked at again. - /// - /// One step per call, no wait; the enumeration it eventually starts is submit-and-return too. - /// - /// **Ports that read empty first**: a teardown gives back the pool block its device held, and a disk this driver's reset moved arrives on another port wanting one — the controller answers one operation at a time, so an enumeration begun first would reach its Configure Endpoint with the block still claimed. - fn service_ports(&mut self) -> Option { - let now = crate::clock::nanos_since_boot(); - let mut wake: Option = None; - for empty in [true, false] { - for p in 0..self.max_ports { - if self.read_portsc(p).connected() == empty { - continue; - } - wake = earliest(wake, self.service_port(p, now)); - } - } - wake - } - - /// One port's step, and when it next wants one. - /// - /// The decision is [`PortState::step`]'s; every line here is an effect, and the register is re-read after each one. - /// - /// The bound is not a timeout — it catches a machine that loops, since one pass issues at most one effect per state it leaves. - fn service_port(&mut self, port_idx: u8, now: u64) -> Option { - const MAX_EFFECTS: usize = 16; - for _ in 0..MAX_EFFECTS { - let portsc = self.read_portsc(port_idx); - if !portsc.holds() { - self.cancel_recovery_on(port_idx); - device::cancel_on(self, port_idx); - } - // A port inside a prior pass's effect is not decided about until the controller answers. - // - // The `expect` is a driver bug, not a device one: the only effect that outlives a pass is the one that filled the slot. - if self.ports[port_idx as usize].working().is_some() { - let at = self.outstanding.wake_at().expect( - "a port is inside an effect the controller was never asked to perform", - ); - return Some(at); - } - // Read before the machine is asked, since its own borrow of this port is live after. - let busy = self.outstanding.wake_at(); - match self.ports[port_idx as usize].step(portsc, now) { - Step::Idle => return None, - Step::Wait(at) => return Some(at), - Step::GaveUp(why) => { - match why { - GaveUp::ResetNeverFinished => log!( - "xHCI: port {} never finished its hot reset (PORTSC {:#010x}); \ - skipping it", - port_idx + 1, - portsc.raw() - ), - // §4.19.1.2 has nothing further after a warm reset — this is the port's end. - GaveUp::LinkNeverTrained => log!( - "xHCI: port {} is SuperSpeed and its link would not train, warm reset \ - included (PORTSC {:#010x}, link {:?}); skipping it", - port_idx + 1, - portsc.raw(), - portsc.link_state() - ), - GaveUp::ResetFailed => log!( - "xHCI: port {} completed its hot reset without enabling \ - (PORTSC {:#010x}); skipping it", - port_idx + 1, - portsc.raw() - ), - } - // No return: the port is left to be read, and nothing else wakes a pass for it. - } - Step::Write(write) => self.write_portsc(port_idx, write), - Step::Reset(kind, write) => { - match kind { - Reset::Hot => log!("xHCI: port {} connected", port_idx + 1), - Reset::Warm => log!( - "xHCI: port {} warm reset, link was {:?}", - port_idx + 1, - portsc.link_state() - ), - } - self.write_portsc(port_idx, write); - } - Step::Teardown(why, pending) => { - if busy.is_some() { - return busy; - } - pending.running(); - match why { - Gone::Disconnected => log!("xHCI: port {} disconnected", port_idx + 1), - Gone::Replugged => log!( - "xHCI: port {} was unplugged and plugged back in between two looks; \ - tearing the old device down before enumerating what is there now", - port_idx + 1 - ), - } - if self.teardown_port(port_idx) { - self.ports[port_idx as usize].torn_down(); - } else { - // The slot is outstanding; this port is inside an effect until the controller answers. - return self.outstanding.wake_at(); - } - } - Step::Enumerate { after, pending } => { - // A slot pending Disable may be handed straight back by Enable Slot below; deferring avoids zeroing the new device's DCBAA entry. - if busy.is_some() { - return busy; - } - pending.running(); - if after.is_none() { - // No reset needed: a SuperSpeed link trains itself, and resetting an already-Enabled port puts it into Inactive with no way back. - log!("xHCI: port {} connected, link already trained", port_idx + 1); - } - device::begin(self, port_idx, after); - // No return: a `begin` that refused before Enable Slot left the port to be read, and a begun one is caught above as working. - } - } - } - log!("xHCI: port {} produced {MAX_EFFECTS} effects without settling; leaving it", - port_idx + 1); - None - } - - /// Everything a device that is no longer on the bus leaves behind: input first, then the slot, then the pool block — safe only in that order, since the slot's endpoint contexts still name that memory while it lives. - /// - /// `true` when the port is already empty; `false` when the controller still has to answer for the slot, and [`Self::slot_gone`] finishes it. - fn teardown_port(&mut self, port_idx: u8) -> bool { - while let Some(at) = self.devices.iter().position(|d| d.port_idx == port_idx) { - let mut dev = self.devices.remove(at); - dev.unbind(); - match &dev.role { - hid::HidRole::Keyboard(_) => log!( - "xHCI: USB keyboard on slot {} unplugged from port {}", - dev.slot_id, port_idx + 1 - ), - // The source is logged because it is the only place the button merge is visible; a leaked entry reads the same otherwise. - hid::HidRole::Pointer(source, _) => log!( - "xHCI: USB pointer on slot {} unplugged from port {}, source {} released", - dev.slot_id, port_idx + 1, source.id() - ), - } - } - let Some(slot) = self.ports[port_idx as usize].take_slot() else { - self.release_blocks(port_idx); - return true; - }; - self.submit_disable_slot(slot.get(), AfterSlot::Teardown(port_idx)); - false - } - - /// The pool blocks a port's device held, back in the pool — after the slot and never before, since the slot's endpoint contexts still name this memory. - fn release_blocks(&mut self, port_idx: u8) { - for at in 0..MSC_BLOCKS { - if self.msc[at].port != Some(port_idx) { - continue; - } - // A disk this driver's reset lost is held under its number; any other's number does not come back, since a mount holds it for the disk's whole life. - if self.msc[at].disk.is_some() { - self.hold_for_return(at, "its port disconnected"); - } - match core::mem::replace(&mut self.msc[at], MscBlock::FREE).disk { - Some(disk) => log!("usb-storage: disk {} unplugged from port {}; it is offline", - disk.index, port_idx + 1), - None if self.awaited.iter().any(|a| a.port_idx == port_idx) => log!( - "usb-storage: port {}'s pool block is free again; the disk it held is waiting \ - for its device", port_idx + 1), - None => log!("usb-storage: the device this driver refused on port {} is gone; \ - its pool block is free again", port_idx + 1), - } - } - } - - /// Take the disk on pool block `at` out of service and hold its number for its device to come back, if its port was reset by this driver inside `toyos_xhci::identity::RETURN_WINDOW`; otherwise leave it where it is. `why` is how its going was seen. - /// - /// The block stays claimed by its port until that port's teardown: the slot's endpoint contexts still name it. - fn hold_for_return(&mut self, at: usize, why: &str) { - let now = crate::clock::nanos_since_boot(); - let Some(disk) = self.msc[at].disk else { return }; - let Some(returns_by) = disk.dev.returns_by(now) else { return }; - self.msc[at].disk = None; - let port_idx = disk.dev.port_idx(); - self.awaited.push(Awaited { - index: disk.index, - identity: *disk.dev.identity(), - port_idx, - returns_by, - owed_flush: disk.dev.owes_a_flush(), - losses: disk.dev.losses_left(), - }); - log!( - "usb-storage: disk {} left port {} ({why}) after this driver reset it; it is held {} ms \ - for the same device to come back", - disk.index, - u32::from(port_idx) + 1, - (returns_by - now) / 1_000_000 - ); - } - - /// The number of a disk held for its device, if the device that bound as `identity` on `port_idx` is it, the loss count it handed on and whether it left owing a flush; the record is spent when it matches. Every held disk it is not says why. - /// - /// Asked at the end of a bind, and every record still here is one the enumeration that began it was inside the window of ([`Self::forget_the_unreturned`]): a device's own bind is not what makes it late. - fn adopt(&mut self, identity: &toyos_xhci::identity::Identity, port_idx: u8) -> Option<(usize, u64, bool)> { - let mut adopted = None; - self.awaited.retain(|held| { - if adopted.is_some() { - return true; - } - match toyos_xhci::identity::same(&held.identity, identity) { - Ok(()) => { - adopted = Some((held.index, held.losses, held.owed_flush)); - false - } - Err(why) => { - log!("usb-storage: the device on port {} is not disk {} come back: {why}", - u32::from(port_idx) + 1, held.index); - true - } - } - }); - adopted - } - - /// Whether a disk on this controller is held for its device. - pub(super) fn awaits_a_device(&self) -> bool { - !self.awaited.is_empty() - } - - /// Every held disk whose window has passed, lost for good; answers when the next one's does. - /// - /// **None while a device has arrived and is not yet bound** — a port that reads connected and that the port machine has not taken, or an enumeration under way: this runs before any of them is acted on, so every record left is one the device arrived inside the window of, and it may be that disk's. The CPU that would enumerate it may be spinning in a call held for it, and its bind — a stick slow to become ready after a reset, the whole of `READY_BUDGET` — may be slow: neither is the device arriving late. - fn forget_the_unreturned(&mut self, now: u64) -> Option { - let enumerating = - matches!(self.outstanding.what(), Some(What::SlotWanted { .. } | What::Enumerating(_))); - let arrived = (0..self.max_ports) - .any(|p| self.read_portsc(p).connected() && !self.ports[p as usize].attached()); - if enumerating || arrived { - return self.awaited.iter().map(|held| held.returns_by).filter(|by| *by > now).min(); - } - self.awaited.retain(|held| { - let waiting = now < held.returns_by; - if !waiting { - log!("usb-storage: disk {} did not come back within {} ms of its port reset; it \ - is offline", held.index, toyos_xhci::identity::RETURN_WINDOW / 1_000_000); - } - waiting - }); - self.awaited.iter().map(|held| held.returns_by).min() - } - - /// Ask the controller for a slot back, and record what its answer is owed. - /// - /// **The one Disable Slot site.** The slot may be in any state, but its endpoints may not: xHCI 1.2 §4.6.4's note has them Stopped, or Running with nothing to run, before the command — which a caller giving back a device that is still on the bus owes first. - fn submit_disable_slot(&mut self, slot_id: u8, then: AfterSlot) { - let mut disable = Trb::ZERO; - disable.control = TRB_DISABLE_SLOT | ((slot_id as u32) << 24); - let on = Await::Command { trb: self.submit_command(disable) }; - self.outstanding.submit(What::SlotGone { slot: slot_id, then }, on, Stages::One, deadline()); - } - - /// The slot is the controller's again, or it is not and there is no second question to ask about it. - fn slot_gone(&mut self, slot: u8, then: AfterSlot, outcome: Outcome) { - if outcome.succeeded() { - // After the command, never before: the controller may still be writing the output context until it completes. - self.write_dcbaa(slot as usize, 0); - log!("xHCI: slot {slot} disabled"); - } else { - log!("xHCI: Disable Slot failed: {}", Answer(outcome)); - } - // Blocks go back whatever the controller said: leaving them held for the boot's life on a repeated refusal exhausts the pool. - match then { - AfterSlot::Teardown(port_idx) => { - self.release_blocks(port_idx); - self.ports[port_idx as usize].torn_down(); - } - AfterSlot::Again(port_idx) => { - // Only a block no disk came of: this port's device was refused. - for block in self.msc.iter_mut().filter(|b| b.port == Some(port_idx) && b.disk.is_none()) { - *block = MscBlock::FREE; - } - self.ports[port_idx as usize].torn_down(); - log!("xHCI: port {} is enumerated again while a disk is held for its device", port_idx + 1); - } - AfterSlot::LetGo | AfterSlot::Refused => {} - } - } - - /// Act on whatever the controller has answered, and issue whatever that answer owes next. - /// - /// Never called from inside a wait: everything below submits commands and frees memory. - fn advance_outstanding(&mut self) { - let now = crate::clock::nanos_since_boot(); - while let Some((what, outcome)) = self.outstanding.finished(now) { - match what { - What::SlotGone { slot, then } => self.slot_gone(slot, then, outcome), - What::Recovering { slot_id, seq, issued } => { - self.recovery_stepped(slot_id, seq, issued, outcome) - } - What::SlotWanted { port_idx, speed, usb_speed, packet, seq } => { - device::slot_answered(self, port_idx, speed, usb_speed, packet, seq, outcome) - } - What::Enumerating(state) => device::stepped(self, state, outcome), - } - } - } - - /// Drain the event ring and step the ports, and say when this controller wants to be polled again. - /// - /// Ports are read only when something says they might have moved; otherwise this costs one event-ring read. - fn poll(&mut self) -> Option { - let _ = self.drain_events(); - // After the drain, not inside it: an answer the drain recorded is issued where nobody else waits on this ring. - self.advance_outstanding(); - self.recover_endpoints(); - self.give_back_offline_slots(); - let returns = self.forget_the_unreturned(crate::clock::nanos_since_boot()); - - // Nothing below reads the event ring: every step `service_ports` takes is a submit, so one advance is enough. - let mut wake_at = None; - if portmachine::due(self.ports_dirty, &self.ports) { - self.ports_dirty = false; - wake_at = self.service_ports(); - } - earliest(earliest(wake_at, self.outstanding.wake_at()), returns) - } - - /// One dword of an input context: index 0 is the control context, 1 the slot context, `dci + 1` an endpoint's. - /// - /// `Endpoint::dci`'s field is private and 2..=31 by construction, bounding the largest reachable index to 32 — without it a struct literal could write 12,880 bytes in. - fn write_ctx32(&self, ctx: Dma<'static>, ctx_index: usize, dword: usize, val: u32) { - let offset = (ctx_index * self.context_size) + (dword * 4); - // Volatile: the controller reads the input context as soon as the command naming it is enqueued. - ctx.write::(offset, val) - } - - /// The Endpoint State the controller published for (`dev_block`'s device, `dci`). - /// - /// Output contexts index endpoints by DCI directly, unlike the input context, whose Input Control Context shifts everything by one. - fn endpoint_state(&self, dev_block: usize, dci: u8) -> EndpointState { - let at = dev_block + DEV_OUT_CTX + dci as usize * self.context_size; - // Volatile: the controller writes this dword by DMA, so it must be re-read rather than cached. - EndpointState::decode(self.dma().read::(at)) - } - - fn advance_event_ring(&mut self) { - self.event_head = (self.event_head + 1) % RING_SIZE as u16; - if self.event_head == 0 { - self.event_phase = !self.event_phase; - } - let erdp = self.dma().device_addr() + OFF_EVT_RING as u64 + (self.event_head as u64) * 16; - self.rt_base.write_u64(IR0_ERDP, erdp | (1 << 3)); // EHB clears interrupt pending - self.rt_base.write_u32(IR0_IMAN, 3); // clear IP (W1C) + keep IE - } - - /// Take at most one ring's worth of events off the event ring. - /// - /// **A bound on a loop whose exit is the controller's.** `next_event` - /// answers for as long as the controller keeps producing, so a bare - /// `while let` here is a scheduler pass that does not return on a ring - /// being refilled — with `XHCI` held and preemption off for all of it. - /// One ring is what the ring could hold when the pass began; anything after - /// that belongs to the next pass, which the interrupt that recorded it - /// brings about. - /// - /// Answers whether the controller said anything, which is what a caller - /// bounding *silence* rather than work re-arms on. - fn drain_events(&mut self) -> bool { - let mut answered = false; - for _ in 0..RING_SIZE { - let Some(event) = self.next_event() else { return answered }; - self.dispatch_event(event); - answered = true; - } - true - } - - fn ring_doorbell(&self, slot: u8, dci: u8) { - // An `Mmio` write: ordered after every TRB enqueued before it. - self.db_base.write_u32(slot as u64 * 4, dci as u32); - } -} - -/// Every xHCI controller on the machine, in PCI enumeration order. -/// -/// A `Vec` and not an `Option`: the target laptop has two, and its own ports hang off the second. -/// -/// Owed, so a CPU whose [`poll_if_pending`] finds it taken halts on the record -/// it leaves instead of spinning a pass on it for as long as a disk transfer -/// holds the lock. -static XHCI: OwedLock> = OwedLock::new(Vec::new(), bring_to_pass); - -/// [`XHCI`]'s answer: every CPU in `owed` takes a scheduler pass, whose -/// [`poll_if_pending`] finds the record it left. -fn bring_to_pass(owed: u64) { - use kernel::sched::hw::{CpuId, Machine}; - for cpu in (0..u64::BITS).filter(|cpu| owed & (1 << cpu) != 0) { - crate::hw::HW.need_resched(CpuId(cpu)); - } -} - -/// Empty every USB disk's write cache, on the way to a reset. -/// -/// **Above the boot's last word**, because these are ordinary records and the -/// volume that carries them is still there. What comes after — the barrier and -/// the register stop — is [`seal_shut`] and [`stop::before_reset`], and both run -/// below that word. -/// Whether the machine's `index`-th disk has ever answered SYNCHRONIZE CACHE -/// with INVALID COMMAND OPERATION CODE. `None` for an index naming no disk. -/// -/// **`Ok(())` from a flush spells two different facts**: a cache emptied, and a -/// device that had none to empty and said so. A shutdown line that renders both -/// as `ok` cannot be read on a machine where only the second ever happens, and -/// on the T14 only the second does. -fn storage_has_no_cache(index: usize) -> bool { - with_disk(index, |ctrl, at| ctrl.msc[at].disk.is_some_and(|d| d.dev.refused_flush())) - .unwrap_or(false) -} - -pub fn flush_disks() { - // **Bounded before the first flush, because `with_disk` takes the lock - // without a bound.** A driver that has stopped answering is one this would - // otherwise spin on until the ticket lock's own deadlock panic, and a - // shutdown that never reaches its reset is worse than a cache that was not - // emptied — the register stop below saves the device either way. Once the - // lock has been seen free, every holder after it is a live operation and - // `block::OPERATION` is what bounds that. - if !lock_settles() { - log!( - "usb-quiesce: the controller lock was not free within {} ms, so no disk cache was \ - emptied and the reset stops the controllers regardless", - BARRIER.millis() - ); - stop::flushed(0, 0, 0); - return; - } - let (mut disks, mut flushed, mut cacheless) = (0u32, 0u32, 0u32); - // Each flush is one block-layer operation and takes the controller lock for - // itself, the way every other caller does. - for index in 0..storage_count() { - let _op = crate::block::begin_operation(); - let mut losses = 0; - let outcome = storage_flush(index, &mut losses); - // A disk that lost writes no writer was told of is not flushed, whatever - // its cache says now: this line is the last that can say so. - let untold = crate::drivers::usb_storage::untold(index, losses); - // Read after the flush, because the flush is what sets it on a device - // that had not been asked before. - let no_cache = outcome.is_ok() && storage_has_no_cache(index); - disks += 1; - flushed += u32::from(outcome.is_ok() && !untold); - cacheless += u32::from(no_cache && !untold); - match (untold, no_cache) { - (true, _) => log!( - "usb-quiesce: disk {index} SYNCHRONIZE CACHE {}, but {UNTOLD}", - Flushed(outcome) - ), - (false, true) => log!( - "usb-quiesce: disk {index} implements no SYNCHRONIZE CACHE, so it owed none" - ), - (false, false) => { - log!("usb-quiesce: disk {index} SYNCHRONIZE CACHE {}", Flushed(outcome)) - } - } - } - // Not logged here: the summary belongs beside what the register stop did, - // and that is written into the black box from below the boot's last word. - stop::flushed(disks, flushed, cacheless); -} - -/// What the shutdown says of a disk that lost writes no writer was told of. -const UNTOLD: &str = "writes its device reported complete before it left owing a flush were \ - lost and no writer's flush has said so; the disk is not counted flushed"; - -/// How long the shutdown waits for the controller lock before going on without -/// it. -/// -/// `block::OPERATION` is what bounds the holder — one block-device operation is -/// refused once it expires, and that is the longest anything holds this lock — -/// and twice it covers one more waiter already ticketed ahead of this one. -const BARRIER: crate::time::Duration = - crate::time::Duration::from_millis(2 * crate::block::OPERATION.duration().millis()); - -/// Take the controller lock and never give it back. -/// -/// **This is the barrier the reset needs.** `with_disk` holds this lock across a -/// whole transfer, so acquiring it means none is in flight, and never releasing -/// it means none can start before the machine ends. Preemption stays disabled -/// with it, which is the same statement about this CPU. -/// -/// **Bounded, and `false` rather than a wait that does not end.** A lock this -/// never gets is a driver that has stopped answering, and a shutdown that -/// waited for one for ever is a machine nobody can turn off — which is worse -/// than the transfer the barrier exists to end. `stop::before_reset` runs -/// either way and the account says which of the two it was. -/// -/// Called from the shutdown path only, and after the log volume's last byte is -/// durable — before that, `logkeeper` still has that volume to write. -pub fn seal_shut() { - match take_within(BARRIER.nanos()) { - Some(guard) => { - #[expect(clippy::disallowed_methods, reason = "the lock never given back is the barrier no transfer starts past")] - core::mem::forget(guard); - stop::barrier(stop::Barrier::Taken); - } - None => stop::barrier(stop::Barrier::Refused), - } -} - -/// Whether the controller lock was free at any point inside [`BARRIER`], with -/// nothing kept. The probe [`flush_disks`] makes before it takes the lock the -/// ordinary way. -fn lock_settles() -> bool { - take_within(BARRIER.nanos()).is_some() -} - -/// `try_lock` until it succeeds or `bound` nanoseconds pass. -/// -/// **The one bounded acquisition the shutdown makes**, and the one place a lock -/// that never comes free can be staged. `XHCI.lock()` spins until its own -/// deadlock panic, and a shutdown path that panicked instead of resetting is a -/// machine nobody can turn off. Not fair — a competitor taking a ticket wins — -/// which is why both callers say what they do without it. -fn take_within(bound: u64) -> Option>> { - let until = crate::clock::counter_deadline(bound); - loop { - if let Some(guard) = XHCI.try_lock() { - return Some(guard); - } - if crate::arch::cpu::counter() >= until { - return None; - } - core::hint::spin_loop(); - } -} - -/// A flush outcome as one word, so the shutdown line reads the same whichever -/// way the disk answered. -struct Flushed(crate::block::BlockResult); - -impl fmt::Display for Flushed { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.0 { - Ok(()) => f.write_str("ok"), - Err(crate::block::BlockError::BudgetExpired) => { - f.write_str("ran out of its operation budget") - } - Err(crate::block::BlockError::Device) => f.write_str("failed"), - } - } -} - -/// Process xHCI events if this CPU has an unserviced interrupt record, or a port's state machine is due. -/// -/// Every controller is polled, not just the one that interrupted, because every controller shares the same vector and `irq_ring` records only that an xHC interrupted, never which. -/// -/// Thread context only, and `drain_irqs`'s alone to call: it takes `XHCI`, a ticket spinlock with preemption off for its life, and can spin on deadlines measured in seconds while holding it. -pub fn poll_if_pending() { - let interrupted = crate::irq_ring::pending(crate::irq_ring::IrqSource::Xhci); - if !interrupted { - match PORT_WORK_AT.load(Ordering::Relaxed) { - 0 => return, - at if crate::clock::nanos_since_boot() < at => return, - _ => {} - } - } - // Decline rather than queue: `lock()` here would put every CPU with work due on one ticket spinlock. - let Some(mut guard) = XHCI.try_lock_or_owe(crate::arch::percpu::cpu_id()) else { return }; - // Taken only now the work will be done: taking it before a declined lock would drop a wake with nothing left to re-post it. - crate::irq_ring::take(crate::irq_ring::IrqSource::Xhci); - let mut wake_at: Option = None; - for ctrl in guard.iter_mut() { - if let Some(at) = ctrl.poll() { - wake_at = Some(wake_at.map_or(at, |w: u64| w.min(at))); - } - } - PORT_WORK_AT.store(wake_at.unwrap_or(0), Ordering::Relaxed); -} - -/// Everything above the controller needs to know about one bound disk. -#[derive(Clone, Copy, Debug)] -pub struct StorageGeometry { - /// What the device itself addresses in, straight off READ CAPACITY. - pub logical_block_bytes: u32, - /// The same capacity in the 4 KiB blocks `BlockDevice` is written in. - pub blocks: u64, -} - -/// Every device a controller bound, as `SYS_DEVICE_INVENTORY` records it. -pub fn inventory() -> Vec { - use toyos_abi::inventory::{Usb, UsbFunction}; - let segment = crate::pcidev::segment(); - let mut out = Vec::new(); - for ctrl in XHCI.lock().iter() { - let controller = crate::pcidev::addr_of(segment, crate::pcidev::requester(&ctrl.pci)); - for hid in &ctrl.devices { - out.push(Usb { - controller, - port: hid.port_idx + 1, - speed: hid.speed, - vendor: hid.usb.vendor, - product: hid.usb.product, - function: match hid.role { - hid::HidRole::Keyboard(_) => UsbFunction::Keyboard, - hid::HidRole::Pointer(..) => UsbFunction::Pointer, - }, - }); - } - for disk in ctrl.msc.iter().filter_map(|block| block.disk) { - let (port_idx, speed, usb) = disk.dev.inventory(); - out.push(Usb { - controller, - port: port_idx + 1, - speed, - vendor: usb.vendor, - product: usb.product, - function: UsbFunction::Storage, - }); - } - } - out -} - -/// How many disk numbers this machine has issued; every value below it names a disk bound at some point in this boot. -pub fn storage_count() -> usize { - DISKS_BOUND.load(Ordering::Relaxed) -} - -/// Run `f` against the machine's `index`-th disk, wherever it is — a search, since neither its controller nor its block is derivable from the machine-wide number. -fn with_disk(index: usize, f: impl FnOnce(&mut XhciController, usize) -> R) -> Option { - on_disk(XHCI.lock(), index, f) -} - -/// [`with_disk`], with the lock taken before `by` or not at all (the outer -/// `None`); `None` for `by` takes it as `with_disk` does. -pub(super) fn with_disk_by( - index: usize, - by: Option, - f: impl FnOnce(&mut XhciController, usize) -> R, -) -> Option> { - let guard = match by { - None => XHCI.lock(), - Some(by) => take_within(by.saturating_sub(crate::clock::nanos_since_boot()))?, - }; - Some(on_disk(guard, index, f)) -} - -fn on_disk( - mut guard: OwedGuard<'static, Vec>, - index: usize, - f: impl FnOnce(&mut XhciController, usize) -> R, -) -> Option { - for ctrl in guard.iter_mut() { - if let Some(at) = ctrl - .msc - .iter() - .position(|block| block.disk.is_some_and(|d| d.index == index)) - { - return Some(f(ctrl, at)); - } - } - None -} - -/// The geometry of the machine's `index`-th disk, `None` where there is no such disk — including, after an unplug, an index that used to name one. -pub fn storage_geometry(index: usize) -> Option { - with_disk(index, |ctrl, at| Some(ctrl.msc[at].disk?.dev.geometry())).flatten() -} diff --git a/kernel/src/drivers/xhci/stop.rs b/kernel/src/drivers/xhci/stop.rs deleted file mode 100644 index c05282c5371..00000000000 --- a/kernel/src/drivers/xhci/stop.rs +++ /dev/null @@ -1,716 +0,0 @@ -//! What every reset this kernel performs does to USB before it lets the -//! machine go. -//! -//! **A reset is not a way to end a transfer.** A platform reset stops the -//! controller mid-Bulk-Only-Transport with a device holding half a command, and -//! it leaves VBUS up, so the device never sees a power cycle either: the next -//! host finds a mass-storage device it cannot enumerate, and rebooting the -//! machine does not clear it. What ends a transfer is a **port** reset driven -//! by a controller that is still there to drive it — xHCI 1.2 §5.4.8's PORTSC.PR -//! over §4.19.5's reset signalling — which returns the device to its Default -//! state (USB 2.0 §7.1.7.5) and with it abandons whatever command it was -//! inside. -//! -//! **And a port reset is not free either.** Bulk-Only Transport §5.1 makes a -//! command three transfers and §6.7.2/§6.7.3 leave a device waiting for -//! whichever has not arrived; §5.3.4 makes recovering from that the device's -//! obligation, through a class request and two CLEAR_FEATURE(HALT)s that a -//! wedged kernel cannot issue. **So this path does not finish the command**: it -//! gives the driver a bound to close it, and then records which phase the -//! device was left in and what the controller was doing. Finishing it would -//! mean writing a TRB onto a ring rebuilt from published numbers, which on -//! every path but the panic one a live driver may still be enqueuing on — for -//! an out data phase, a second write of the block. What a cut command then -//! costs is a question about the device and not about this path: -//! `issues/a-cut-bulk-only-command-does-not-brick-the-benchs-stick.md`. -//! -//! **Registers and DMA and nothing else, because the panic path calls this.** -//! The controllers are published into [`POINTS`] at bring-up rather than read -//! out of `XHCI`, and the command that is open is published into [`OPEN`] the -//! same way: a panicked CPU may take no lock and allocate nothing, and the CPU -//! it panicked on may be the one holding that lock. Nothing here reads the -//! event ring either, which is the one structure that CPU is still the -//! consumer of. - -use core::fmt; -use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicU8, Ordering}; - -use toyos_xhci::bot::Phase; -use toyos_xhci::{EndpointState, NotOnTheRing, Portsc, Ring}; - -use crate::log; -use crate::mm::{Dma, Mmio}; - -use super::{OP_PORT_BASE, OP_USBCMD, OP_USBSTS, PORTSC_PP, PORT_REG_SIZE, USB_TIMEOUT_NS}; -use super::{USBCMD_HCRST, USBCMD_RS, USBSTS_CNR, USBSTS_HCH}; -use super::{TrbRing, MSC_IN_RING, MSC_OUT_RING}; - -/// Controllers this path has room for. -/// -/// Four, and a fifth is refused by name at bring-up rather than dropped: the -/// target laptop has two and nothing in reach has more, and a controller that -/// silently had no slot is a device this path would leave mid-command. -const SLOTS: usize = 4; - -/// How long one controller's connected ports get to finish their reset. -/// -/// The driver's own transfer bound, because it is the same question — how long -/// this controller may be given to answer at all — and a port that never clears -/// PR is a controller the account then names. -const PORT_RESET_NS: u64 = USB_TIMEOUT_NS; - -/// How long a device gets between the bus reset that ended its transactions and -/// the moment its power goes away. -/// -/// **The one margin in this path rather than a derivation.** USB 2.0 §7.1.7.5 -/// gives a device 10 ms to recover from a reset (TRSTRCY) and says nothing about -/// how long a mass-storage device's own firmware spends finishing what a -/// SYNCHRONIZE CACHE started; this is ten times TRSTRCY, and it is spent once, -/// at a reset, with nothing else left to do. -const DEVICE_RECOVERY_NS: u64 = 100_000_000; - -/// One controller's registers, as numbers a lock-free reader can hold. -struct Point { - /// The operational register window's address. **Zero until published, and - /// stored last**, so a reader that sees it sees every field beside it. - op: AtomicU64, - op_bytes: AtomicU64, - /// This function's PCI config window, for the bus-master bit. - config: AtomicU64, - /// `bus << 24 | dev << 16 | func << 8 | max_ports`. - shape: AtomicU32, -} - -impl Point { - // The lint's case is a `const` *read* through, which copies the atomics - // instead of sharing them. This one is only ever the initialiser of the - // `static` below, which is the array's own storage. - #[allow(clippy::declare_interior_mutable_const)] - const EMPTY: Self = Self { - op: AtomicU64::new(0), - op_bytes: AtomicU64::new(0), - config: AtomicU64::new(0), - shape: AtomicU32::new(0), - }; -} - -static POINTS: [Point; SLOTS] = [Point::EMPTY, Point::EMPTY, Point::EMPTY, Point::EMPTY]; - -/// What the shutdown's flush pass emptied, for the one summary line. -/// -/// **Two atomics and not a return value.** The flush runs above the boot's last -/// word, where a record still reaches the file; this summary is written -/// below it. Nothing carries a value across that, and a reset -/// that flushed nothing — every panic — reads the zeros it was born with. -static DISKS: AtomicU32 = AtomicU32::new(0); -static FLUSHED: AtomicU32 = AtomicU32::new(0); -static CACHELESS: AtomicU32 = AtomicU32::new(0); - -/// What [`super::flush_disks`] emptied, and how many of those disks had no cache -/// to empty — which is what an `ok` from such a device means and what a line -/// spelling both the same way cannot say. -pub(super) fn flushed(disks: u32, ok: u32, cacheless: u32) { - DISKS.store(disks, Ordering::Relaxed); - FLUSHED.store(ok, Ordering::Relaxed); - CACHELESS.store(cacheless, Ordering::Relaxed); -} - -/// Whether a transfer could still have been in flight when the registers below -/// were stopped. -/// -/// **The one thing the account cannot read off a register.** A port reset ends a -/// transfer whatever state it was in, so this decides how much the stop had to -/// do — not whether it happened. -#[derive(Clone, Copy)] -pub(super) enum Barrier { - /// No shutdown path ran: this reset came from a panic, where every other - /// CPU was stopped by an NMI where it stood, which is not the same claim. - NotTried = 0, - /// The shutdown held the controller lock from before the log volume's last - /// durable byte, so nothing was in flight and nothing could start. - Taken = 1, - /// The lock was not free inside its bound. The reset happens anyway: a - /// machine that cannot be turned off is worse than a transfer cut short. - Refused = 2, -} - -static BARRIER: AtomicU8 = AtomicU8::new(Barrier::NotTried as u8); - -/// What [`super::seal_shut`] managed, for the account this module writes. -pub(super) fn barrier(taken: Barrier) { - BARRIER.store(taken as u8, Ordering::Relaxed); -} - -impl Barrier { - fn said(self) -> &'static str { - match self { - Self::NotTried => "usb-quiesce: no barrier was taken, so this reset is not the \ - shutdown's", - Self::Taken => "usb-quiesce: the controller lock was held from before the log \ - volume's last durable byte, so no transfer was in flight", - Self::Refused => "usb-quiesce: the controller lock was not free inside its bound, \ - so a transfer may have been in flight", - } - } - - fn of(raw: u8) -> Self { - match raw { - 1 => Self::Taken, - 2 => Self::Refused, - _ => Self::NotTried, - } - } -} - -/// The one Bulk-Only command this kernel can have open, as the numbers a -/// lock-free reader can hold. -/// -/// **The one thing [`before_reset`] must know and cannot ask.** Its reader takes -/// no lock — the CPU holding `XHCI` may be the wedged one this reset is ending, -/// which is the whole reason [`POINTS`] exists — so the command is published -/// beside the driver rather than read out of it. -/// -/// **One and not one per disk.** `with_disk` holds `XHCI` across a whole round -/// trip and the boot scan runs before any AP exists, so no two commands can be -/// open at once; [`OpenCommand::begin`] fails fast on a second rather than -/// overwriting the first, since a command this record did not name is exactly -/// the device a reset would leave mid-phase. -struct Open { - /// [`Phase::code`], and the release store that publishes every field - /// beside it: a reader that sees an open phase sees the rest. - phase: AtomicU8, - /// The device's block in its controller's DMA pool, as the address that - /// controller was programmed with. Both bulk rings live at fixed offsets - /// inside it. - block_device: AtomicU64, - /// `slot << 16 | in_dci << 8 | out_dci`. - endpoints: AtomicU32, - /// Which way the data phase moves, which is the ring it goes on. - data_in: AtomicBool, - /// Each bulk ring's next enqueue point, the in ring in the low half and - /// the out ring in the high one. - rings: AtomicU32, - /// The device's own context block, as one - /// [`Dma::addr`]/[`Dma::device_addr`] pair, and the width of one context in - /// it. **What the controller says**, against what the driver queued: the - /// endpoint state and the dequeue pointer it has reached are the only - /// account of what a reset found the hardware doing. - ctx: AtomicU64, - ctx_device: AtomicU64, - ctx_size: AtomicU32, -} - -static OPEN: Open = Open { - phase: AtomicU8::new(0), - block_device: AtomicU64::new(0), - endpoints: AtomicU32::new(0), - data_in: AtomicBool::new(false), - rings: AtomicU32::new(0), - ctx: AtomicU64::new(0), - ctx_device: AtomicU64::new(0), - ctx_size: AtomicU32::new(0), -}; - -/// The device one Bulk-Only round trip is about to run on, as the account has -/// to see it. Nothing here is acted on: [`Self::ctx`] is what the *controller* -/// says and the rest is what the driver queued, and the account is the whole -/// use either has. -pub(in crate::drivers::xhci) struct Device { - pub block: Dma<'static>, - pub slot: u8, - pub in_dci: u8, - pub out_dci: u8, - pub data_in: bool, - /// The device's own context block, and the width of one context in it. - pub ctx: Dma<'static>, - pub ctx_size: u32, -} - -/// Raised for exactly as long as one Bulk-Only command is open on a device. -/// -/// **Its lifetime is the command's and not a transfer's.** A device that has -/// taken a CBW and is waiting for its data or its CSW holds no transfer this -/// kernel is inside (BOT §5.1), and that gap — between two of the three phases — -/// is the state a reset's account could otherwise not name at all. -#[must_use = "the command counts as open for exactly as long as this lives"] -pub(in crate::drivers::xhci) struct OpenCommand(()); - -impl OpenCommand { - /// Publish the device this round trip runs on, with no phase reached yet. - /// - /// Called once the CBW is built and before its transfer is queued, so - /// nothing reaches the controller under a stale device. - pub(in crate::drivers::xhci) fn begin(on: Device) -> Self { - assert_eq!( - OPEN.phase.load(Ordering::Acquire), - Phase::Closed.code(), - "xHCI: a second Bulk-Only command was opened while one was still open; \ - `with_disk` holds the controller lock across a whole round trip, so this is a \ - driver bug and the device the record does not name is one a reset would cut" - ); - OPEN.block_device.store(on.block.device_addr(), Ordering::Relaxed); - OPEN.endpoints.store( - (u32::from(on.slot) << 16) | (u32::from(on.in_dci) << 8) | u32::from(on.out_dci), - Ordering::Relaxed, - ); - OPEN.data_in.store(on.data_in, Ordering::Relaxed); - OPEN.ctx.store(on.ctx.addr(), Ordering::Relaxed); - OPEN.ctx_device.store(on.ctx.device_addr(), Ordering::Relaxed); - OPEN.ctx_size.store(on.ctx_size, Ordering::Relaxed); - Self(()) - } - - /// Publish where the two bulk rings now stand and which phase the round trip - /// has reached. - /// - /// Called after every enqueue and before its doorbell, at each gap between - /// two phases, and after any recovery that rebuilt a ring: a transfer is - /// never visible to the controller without the reset being able to see - /// where the ring it went on now is. - pub(in crate::drivers::xhci) fn at(&self, phase: Phase, in_ring: &TrbRing, out_ring: &TrbRing) { - OPEN.rings.store( - u32::from(in_ring.tail) | (u32::from(out_ring.tail) << 16), - Ordering::Relaxed, - ); - // Last, and the release: a reader that sees this phase sees the stores - // above it and the ones `begin` made. - OPEN.phase.store(phase.code(), Ordering::Release); - } -} - -impl Drop for OpenCommand { - fn drop(&mut self) { - OPEN.phase.store(Phase::Closed.code(), Ordering::Release); - } -} - -/// The open command, as the views and numbers the account reads, or `None` -/// where no device is inside one. -fn inside() -> Option { - let phase = Phase::of(OPEN.phase.load(Ordering::Acquire)); - if !phase.open() { - return None; - } - let ctx_size = OPEN.ctx_size.load(Ordering::Relaxed) as usize; - // SAFETY: the two are one live `Dma<'static>`'s own `addr()` and - // `device_addr()`, stored by `begin` over the device context block - // `prepare` placed in the pool the xHCI bring-up leaked for the machine's - // life; a device context is 32 contexts of `ctx_size`. - let ctx = unsafe { - Dma::from_addr( - OPEN.ctx.load(Ordering::Relaxed), - OPEN.ctx_device.load(Ordering::Relaxed), - 32 * ctx_size, - ) - }; - let endpoints = OPEN.endpoints.load(Ordering::Relaxed); - let rings = OPEN.rings.load(Ordering::Relaxed); - Some(Inside { - ctx, - ctx_size, - phase, - slot: (endpoints >> 16) as u8, - in_dci: (endpoints >> 8) as u8, - out_dci: endpoints as u8, - data_in: OPEN.data_in.load(Ordering::Relaxed), - block_device: OPEN.block_device.load(Ordering::Relaxed), - in_tail: rings as u16, - out_tail: (rings >> 16) as u16, - }) -} - -/// A device inside a command, as this path has to see it. -#[derive(Clone, Copy)] -struct Inside { - phase: Phase, - slot: u8, - in_dci: u8, - out_dci: u8, - data_in: bool, - /// The device's block, from which both bulk rings are a fixed offset, and - /// where the driver's next enqueue on each of them goes. - block_device: u64, - in_tail: u16, - out_tail: u16, - /// The device's own context block and the width of one context in it — - /// read for the account and never acted on. - ctx: Dma<'static>, - ctx_size: usize, -} - -impl Inside { - /// Which endpoint the data phase is on, and the ring it went on. - fn data_endpoint(self) -> (u8, Ring) { - let (dci, at, tail) = match self.data_in { - true => (self.in_dci, MSC_IN_RING, self.in_tail), - false => (self.out_dci, MSC_OUT_RING, self.out_tail), - }; - let base = self.block_device + at as u64; - (dci, Ring { base, trbs: super::RING_SIZE as u16, tail }) - } - - /// What the *controller* says about the data endpoint: the state it has it - /// in, and how many TRBs it has not reached on the ring the driver queued — - /// or, where its dequeue pointer is no position on that ring at all, which - /// of the ways it is not one. - /// - /// **The one thing in this account that is not the driver's own claim.** A - /// reset that finds a Running endpoint with TRBs still ahead of its dequeue - /// pointer landed on a controller that was moving bytes; one that finds it - /// Running and caught up landed on an idle bus. Those are two different - /// resets and the device only survives one of them. - fn controller_state(self) -> (EndpointState, Result) { - let (dci, ring) = self.data_endpoint(); - let at = usize::from(dci) * self.ctx_size; - // Volatile through `Dma`: the controller writes both of these by DMA. - let state = EndpointState::decode(self.ctx.read::(at)); - let lo = u64::from(self.ctx.read::(at + 8)); - let hi = u64::from(self.ctx.read::(at + 12)); - (state, ring.pending((hi << 32) | lo)) - } -} - -/// How long the driver gets to close the command it has open. -/// -/// The driver's own transfer bound, because it is the same question — is this -/// device still answering — and past it a longer wait changes nothing the reset -/// then has to do. -const BOT_NS: u64 = USB_TIMEOUT_NS; - -/// Give the driver its bound to close the command it has open, and say what -/// the reset then found. -/// -/// **This path does not put a transfer on the bus.** The rings it can see are -/// rebuilt from numbers [`OpenCommand`] published, and nothing here holds the -/// lock that would stop the driver's own CPU from enqueuing on them — so a TRB -/// written here could be a second write of a block the device already has, -/// which is worse than the reset it would be avoiding. Finishing the command -/// is not the way out the class offers either: BOT §5.3.4 makes reset recovery -/// the device's obligation. -/// -/// So what is left is the account: which phase the device was left in, and what -/// the controller was doing to it. The reset follows either way — a machine -/// nobody can turn off is worse than a device somebody has to replug. -fn settle_commands(said: &mut dyn fmt::Write) { - if inside().is_none() { - let _ = writeln!( - said, - "usb-quiesce: no Bulk-Only command was open, so this reset cuts none" - ); - return; - } - crate::clock::settles(BOT_NS, || inside().is_none()); - // Read again rather than reusing the first: a command that walked a phase - // inside the wait is in the phase it is in now, not the one it was in then. - let Some(open) = inside() else { - let _ = writeln!( - said, - "usb-quiesce: the Bulk-Only command that was open was closed by its own driver \ - inside {} ms, so this reset cuts none", - BOT_NS / 1_000_000, - ); - return; - }; - let (state, pending) = open.controller_state(); - let _ = writeln!( - said, - "usb-quiesce: a Bulk-Only command was open in its {} phase on slot {} after {} ms, so \ - this reset cuts it", - open.phase, - open.slot, - BOT_NS / 1_000_000, - ); - // What the hardware was doing, as against what the driver had queued. A - // reset that finds TRBs still ahead of the controller landed on a bus that - // was moving bytes, and that is the state this account exists to name — so - // a dequeue pointer that is no position on the ring is said as that, never - // wrapped into a count a reader would take for the hardware's own word. - let _ = match pending { - Ok(pending) => writeln!( - said, - "usb-quiesce: the controller had that device's data endpoint {state} with {pending} \ - TRB(s) it had not reached on the ring", - ), - Err(why) => writeln!( - said, - "usb-quiesce: the controller had that device's data endpoint {state} and {why}", - ), - }; -} - -/// Entered once for the machine's life. -/// -/// **A panic inside this path would otherwise reach it a second time.** -static STOPPING: AtomicBool = AtomicBool::new(false); - -/// Record a controller this kernel drives, so a reset can stop it without the -/// lock that guards the driver. -/// -/// Called from the bring-up for every controller that came up, and for none -/// that was refused: a refused controller had no transfer from this kernel on -/// it, and the ones refused after their own halt and HCRST are already stopped. -pub(super) fn publish(op: Mmio, config: Mmio, bus: u8, dev: u8, func: u8, max_ports: u8) { - let shape = (u32::from(bus) << 24) - | (u32::from(dev) << 16) - | (u32::from(func) << 8) - | u32::from(max_ports); - for point in POINTS.iter() { - if point.op.load(Ordering::Relaxed) != 0 { - continue; - } - point.op_bytes.store(op.size(), Ordering::Relaxed); - point.config.store(config.addr(), Ordering::Relaxed); - point.shape.store(shape, Ordering::Relaxed); - // Last, and the release: a reader that sees this address sees the three - // stores above it. - point.op.store(op.addr(), Ordering::Release); - return; - } - log!( - "xHCI: {bus:02x}:{dev:02x}.{func} is past the {SLOTS} controllers a reset can stop, so a \ - reset would leave its devices mid-command — refused" - ); -} - -/// A published controller, as the two windows a stop drives. -#[derive(Clone, Copy)] -struct Live { - op: Mmio, - config: Mmio, - bus: u8, - dev: u8, - func: u8, - max_ports: u8, -} - -impl Point { - fn live(&self) -> Option { - let at = self.op.load(Ordering::Acquire); - if at == 0 { - return None; - } - let shape = self.shape.load(Ordering::Relaxed); - Some(Live { - // SAFETY: `at` and the size beside it are one live `Mmio`'s own - // `addr()`/`size()`, stored by `publish` from the window the - // bring-up mapped and which lives for the machine's life. - op: unsafe { Mmio::from_addr(at, self.op_bytes.load(Ordering::Relaxed)) }, - // SAFETY: the same, for the config window `PciDevice` carries. - config: unsafe { - Mmio::from_addr(self.config.load(Ordering::Relaxed), crate::drivers::pci::CONFIG_BYTES) - }, - bus: (shape >> 24) as u8, - dev: (shape >> 16) as u8, - func: (shape >> 8) as u8, - max_ports: shape as u8, - }) - } -} - -impl Live { - fn portsc_at(self, port: u8) -> u64 { - OP_PORT_BASE + u64::from(port) * PORT_REG_SIZE - } - - /// Reset every connected port, halt the controller, reset it, take the - /// ports' power away and stop its bus mastering. - /// - /// Every step says what it *did* and not what it asked for: each register - /// is read back, because a controller that ignored a write and one that took - /// it are the same instruction and different machines. - fn stop(self, tally: &mut Stopped, said: &mut dyn fmt::Write) { - tally.controllers += 1; - let (bus, dev, func) = (self.bus, self.dev, self.func); - let at = format_args!("{bus:02x}:{dev:02x}.{func}"); - - // **First, and while the controller is still running.** This is the one - // act an attached device sees: PR drives USB reset signalling on the - // port (xHCI 1.2 §4.19.5), and a device that takes it is back in its - // Default state with no command outstanding (USB 2.0 §7.1.7.5). Every - // connected port is set going before any is waited on, because they - // reset in parallel and waiting per port would add their bounds up. - let mut connected: u32 = 0; - for port in 0..self.max_ports { - let portsc = Portsc::from_raw(self.op.read_u32(self.portsc_at(port))); - if !portsc.connected() { - continue; - } - connected += 1; - self.op.write_u32(self.portsc_at(port), portsc.neutral().resetting().raw()); - } - let still_resetting = || { - (0..self.max_ports) - .filter(|port| Portsc::from_raw(self.op.read_u32(self.portsc_at(*port))).in_reset()) - .count() as u32 - }; - if connected > 0 { - crate::clock::settles(PORT_RESET_NS, || still_resetting() == 0); - } - // What the ports did, not what they were asked: a port still holding PR - // after the bound is a controller that never drove the reset. - let finished = connected.saturating_sub(still_resetting()); - tally.connected += connected; - tally.reset_ports += finished; - let _ = writeln!(said, "usb-quiesce: xHCI {at} {finished}/{connected} connected port(s) reset"); - - // The device's own recovery, and whatever its firmware is still doing - // with what the last SYNCHRONIZE CACHE handed it, before anything else - // moves under it. - crate::clock::settles(DEVICE_RECOVERY_NS, || false); - - // Run/Stop cleared: a halted controller is reading no ring and driving - // no endpoint, which is what makes the HCRST below a defined act - // (xHCI 1.2 §5.4.1). - let cmd = self.op.read_u32(OP_USBCMD); - self.op.write_u32(OP_USBCMD, cmd & !USBCMD_RS); - let halted = crate::clock::settles(USB_TIMEOUT_NS, || { - self.op.read_u32(OP_USBSTS) & USBSTS_HCH != 0 - }); - tally.halted += u32::from(halted); - let _ = writeln!( - said, - "usb-quiesce: xHCI {at} halted={halted} USBSTS={:#010x}", - self.op.read_u32(OP_USBSTS) - ); - - // The controller back to its own defaults, so the next kernel — or this - // machine's firmware — finds no ring it did not build (§4.22.1). - self.op.write_u32(OP_USBCMD, USBCMD_HCRST); - let cleared = crate::clock::settles(USB_TIMEOUT_NS, || { - self.op.read_u32(OP_USBCMD) & USBCMD_HCRST == 0 - }); - let ready = crate::clock::settles(USB_TIMEOUT_NS, || { - self.op.read_u32(OP_USBSTS) & USBSTS_CNR == 0 - }); - tally.reset += u32::from(cleared && ready); - let _ = writeln!(said, "usb-quiesce: xHCI {at} reset={cleared} ready={ready}"); - - // Port power away where this controller has switches to take it away - // with, and after HCRST because HCRST puts PORTSC back to its default — - // which on a controller with Port Power Control is unpowered already. A - // controller without it ignores the write, and the read-back count is - // what says which kind this one is. - let mut down: u32 = 0; - for port in 0..self.max_ports { - let off = self.portsc_at(port); - let portsc = Portsc::from_raw(self.op.read_u32(off)); - self.op.write_u32(off, portsc.neutral().unpowered().raw()); - if self.op.read_u32(off) & PORTSC_PP == 0 { - down += 1; - } - } - tally.ports += u32::from(self.max_ports); - tally.unpowered += down; - let _ = writeln!(said, "usb-quiesce: xHCI {at} {down}/{} port(s) unpowered", self.max_ports); - - // Last, because everything above is an MMIO write this path still - // needed: after it the function can issue no DMA at all, so nothing - // stale reaches memory the next kernel owns. - crate::drivers::pci::stop_bus_mastering(self.config); - let _ = writeln!(said, "usb-quiesce: xHCI {at} bus mastering off"); - } -} - -/// Stop every controller this kernel drives, and say what that did. -/// -/// The one entry, taken by the orderly shutdown and by a panic alike. Takes no -/// lock, allocates nothing, and every wait in it is bounded against the CPU's counter. -fn stop_all(said: &mut dyn fmt::Write) { - let mut tally = Stopped::NOTHING; - tally.disks = DISKS.load(Ordering::Relaxed); - tally.flushed = FLUSHED.load(Ordering::Relaxed); - tally.cacheless = CACHELESS.load(Ordering::Relaxed); - let _ = writeln!(said, "{}", Barrier::of(BARRIER.load(Ordering::Relaxed)).said()); - // Before the first register of the first controller: the record is - // machine-wide, and a command on one controller is not made safe by - // stopping another first. - settle_commands(said); - for point in POINTS.iter() { - if let Some(live) = point.live() { - live.stop(&mut tally, said); - } - } - let _ = writeln!(said, "{tally}"); -} - -/// Hand every USB device back, then let the caller end the machine. -/// -/// Makes "no reset this kernel performs leaves a USB device mid-command" a -/// property of the reset rather than of whoever asked for one. -pub fn before_reset() { - if STOPPING.swap(true, Ordering::AcqRel) { - return; - } - // The account goes into the black box, under whatever this boot already - // sealed there. It reaches no file: the log volume's own device is one of - // the ones being taken down, and on the orderly path the volume's last byte - // is already durable. The next boot's loader is its only reader. - if !crate::blackbox::append(|out| stop_all(out)) { - stop_all(&mut Unheard); - } -} - -/// Where the account goes on a boot with no page to put it on. The stop happens -/// either way: it is what the machine needs, and the account is what a reader of -/// the next boot needs. -struct Unheard; - -impl fmt::Write for Unheard { - fn write_str(&mut self, _: &str) -> fmt::Result { - Ok(()) - } -} - -/// What the reset did to this machine's USB. -/// -/// **A count per step and not a verdict**: a controller that halted and one that -/// was merely asked to are the difference this whole path exists for, and the -/// next boot's loader is the only reader. -struct Stopped { - disks: u32, - flushed: u32, - cacheless: u32, - controllers: u32, - connected: u32, - reset_ports: u32, - halted: u32, - reset: u32, - ports: u32, - unpowered: u32, -} - -impl Stopped { - const NOTHING: Self = Self { - disks: 0, - flushed: 0, - cacheless: 0, - controllers: 0, - connected: 0, - reset_ports: 0, - halted: 0, - reset: 0, - ports: 0, - unpowered: 0, - }; -} - -impl fmt::Display for Stopped { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "usb-quiesce: {}/{} disk cache(s) flushed, {} with no cache to flush, \ - {}/{} connected port(s) reset, {}/{} controller(s) halted, {} reset, \ - {}/{} port(s) unpowered", - self.flushed, - self.disks, - self.cacheless, - self.reset_ports, - self.connected, - self.halted, - self.controllers, - self.reset, - self.unpowered, - self.ports - ) - } -} diff --git a/kernel/src/drivers/xhci/wait/boot.rs b/kernel/src/drivers/xhci/wait/boot.rs deleted file mode 100644 index 5b74a7daba7..00000000000 --- a/kernel/src/drivers/xhci/wait/boot.rs +++ /dev/null @@ -1,551 +0,0 @@ -//! Brings up every xHCI controller and runs the one enumeration scan that -//! happens before there is a scheduler; every wait here runs in place. - -use alloc::vec::Vec; - -use crate::log; -use crate::time::{Budget, Cadence, Duration}; -use crate::mm::policy::MmioPolicy; -use crate::mm::Mmio; -use crate::drivers::pci::PciDevice; -use crate::drivers::DmaPool; -use super::super::{device, legacy}; -use super::super::{storage_count, DEV_STRIDE, MSC_BLOCKS, PortState, Outstanding}; -use device::{begin, reset_done, reset_port}; -use super::super::{ErstEntry, Layout, MscBlock, PortMask, Portsc, Protocols, TrbRing}; -use super::super::{XhciController, PAGE, RING_SIZE, USB_TIMEOUT_NS}; -use super::super::{CAP_CAPLENGTH, CAP_DBOFF, CAP_HCCPARAMS1, CAP_HCSPARAMS1, CAP_HCSPARAMS2}; -use super::super::{CAP_RTSOFF, HCC_PPC}; -use crate::arch::trap::XHCI_VECTOR; -use super::super::{IR0_ERDP, IR0_ERSTBA, IR0_ERSTSZ, IR0_IMAN, IR0_IMOD}; -use super::super::{OFF_CMD_RING, OFF_DCBAA, OFF_ERST, OFF_EVT_RING}; -use super::super::{OP_CONFIG, OP_CRCR, OP_DCBAAP, OP_PAGESIZE, OP_PORT_BASE, OP_USBCMD, OP_USBSTS}; -use super::super::{USBCMD_HCRST, USBCMD_RS, USBSTS_CNR, USBSTS_HCH}; -use super::super::{PORTSC_PP, PORT_REG_SIZE, XHCI}; -use super::super::PORT_DEBOUNCE_NS; -use super::settles; -use toyos_xhci::port::{self, GaveUp, Reset, ResetOutcome}; -use toyos_xhci::Protocol; - -/// How long a machine on which *nothing at all* has connected keeps looking, -/// as [`port::EMPTY_BUS_NS`] declares it. -const EMPTY_BUS: Budget = Budget::of( - Duration::from_nanos(port::EMPTY_BUS_NS), - "the scan reports the bus as empty and the boot goes on without whatever was slow", -); - -/// When the driver stops waiting for a root hub that keeps changing its mind. -pub const PORT_SETTLE_CEILING: Budget = Budget::of( - Duration::from_millis(1_500), - "the scan takes whatever is connected at that instant and names the port state", -); - -/// How often the settle re-reads the port registers. -pub const PORT_POLL: Cadence = Cadence::every( - Duration::from_millis(1), - "one MMIO read per port, so sixteen reads per millisecond on the widest controller", -); -// `PORTSC.CCS` is not a question that can be asked at an instant: detecting a -// device is a physical process, so a scan issued right after `USBCMD.R/S` -// reports an empty bus on any machine whose ports are real. -// -// QEMU sets PORTSC from the QOM tree before the guest's first MMIO access, with -// no port state machine of its own — so no test in this suite would catch this -// wait being removed. -// -// Machine-wide, not per controller: the wait is wall-clock, so two controllers -// would otherwise pay for the same debounce twice. -fn await_connect_settle(controllers: &[XhciController]) { - let Some(powered_at) = controllers.iter().map(|c| c.powered_at).max() else { return }; - let mut seen: Vec<(PortMask, u64)> = controllers - .iter() - .map(|c| (c.connected_ports(), c.powered_at)) - .collect(); - - loop { - let now = crate::clock::nanos_since_boot(); - let empty = seen.iter().all(|(mask, _)| *mask == [0u64; 4]); - let debounced = seen - .iter() - .all(|(_, at)| now.saturating_sub(*at) >= PORT_DEBOUNCE_NS); - let looked_long_enough = !empty || now.saturating_sub(powered_at) >= EMPTY_BUS.nanos(); - if debounced && looked_long_enough { - return; - } - if now.saturating_sub(powered_at) >= PORT_SETTLE_CEILING.nanos() { - log!("xHCI: no root hub on this machine held one connect state for {} ms within \ - {} ms; enumerating whatever is connected now", - PORT_DEBOUNCE_NS / 1_000_000, PORT_SETTLE_CEILING.duration().millis()); - return; - } - - let next = now + PORT_POLL.nanos(); - while crate::clock::nanos_since_boot() < next { - core::hint::spin_loop(); - } - for (ctrl, (mask, changed_at)) in controllers.iter().zip(seen.iter_mut()) { - let now_mask = ctrl.connected_ports(); - if now_mask != *mask { - *mask = now_mask; - *changed_at = crate::clock::nanos_since_boot(); - } - } - } -} - -// `None` must stay a refusal, never a degradation: there is no polled mode, and -// every event-ring read depends on `irq_ring`, which only the ISR sets. -fn arm_interrupt(pci_dev: &PciDevice) -> Option<&'static str> { - if pci_dev.enable_msix(XHCI_VECTOR).is_some() { - return Some("MSI-X"); - } - pci_dev.enable_msi(XHCI_VECTOR).then_some("MSI") -} - -/// Brings up every xHCI controller on the machine, not just the first. -pub fn init(devices: &[PciDevice]) { - // Once for the machine: it touches no device, so a second run would repeat itself. - #[cfg(feature = "boot-actuators")] - if crate::actuator::xhci_descriptor_selftest() { - device::selftest(); - } - - // All controllers power on before any is scanned: the settle wait is - // wall-clock and shared, so scanning as each finishes would pay it twice. - let mut controllers = Vec::new(); - let mut present = 0; - for pci_dev in devices.iter().filter(|d| d.matches_class(0x0C, 0x03, Some(0x30))) { - // Before anything that touches the function, `enable_bus_master`'s - // record of it as this kernel's above all: that is what refuses a claim. - if crate::actuator::xhci_left() == Some(crate::pcidev::requester(pci_dev)) { - log!("xHCI: leaving PCI {:02x}:{:02x}.{} {:04x}:{:04x} to a claim (xhci-leave)", - pci_dev.bus, pci_dev.dev, pci_dev.func, pci_dev.vendor_id(), pci_dev.device_id()); - continue; - } - present += 1; - if let Some(ctrl) = init_one(pci_dev) { - controllers.push(ctrl); - } - } - - await_connect_settle(&controllers); - - for ctrl in controllers.iter_mut() { - scan_ports(ctrl); - if ctrl.devices.is_empty() { - log!("xHCI: no HID devices on the controller at {:02x}:{:02x}.{}", - ctrl.pci.bus, ctrl.pci.dev, ctrl.pci.func); - } - } - - if controllers.is_empty() { - // Distinct log lines: a machine with no controller and one whose - // controllers were all refused are different failures. - match present { - 0 => log!("xHCI: no controller on this machine, USB input unavailable"), - n => log!("xHCI: {n} controller(s) present, none of them usable, USB unavailable"), - } - return; - } - 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()); - // 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. - for ctrl in &controllers { - super::super::stop::publish( - ctrl.op_base, - ctrl.pci.config_window(), - ctrl.pci.bus, - ctrl.pci.dev, - ctrl.pci.func, - ctrl.max_ports, - ); - } - // No scheduler pass runs before `smp::set_ready`, so the scan's interrupt record is first polled with `XHCI` published. - *XHCI.lock() = controllers; -} - -// A controller that reports nothing leaves every port UNKNOWN, driven the USB2 way. -fn read_protocols( - bar: &Mmio, - bar_size: u64, - hccparams1: u32, - max_ports: u8, - pci_dev: &PciDevice, -) -> Protocols { - let read = |offset: u64| -> Option { - (offset.checked_add(4)? <= bar_size).then(|| bar.read_u32(offset)) - }; - let mut protocols = Protocols::UNKNOWN; - let mut refused = 0; - let walked = legacy::for_each( - &read, - hccparams1 >> 16, - legacy::CAP_ID_PROTOCOL, - &mut |at| { - let dwords = (read(at), read(at + 4), read(at + 8)); - let (Some(dw0), Some(dw1), Some(dw2)) = dwords else { - refused += 1; - return; - }; - match toyos_xhci::protocol::SupportedProtocol::decode(dw0, dw1, dw2, max_ports) { - Ok(found) => { - log!("xHCI: USB {}.{:x} on ports {}..={}", found.major, found.minor >> 4, - found.first_port + 1, found.first_port + found.port_count); - protocols.record(&found); - } - Err(why) => { - refused += 1; - log!("xHCI: a Supported Protocol capability at {at:#x} is unusable: {why:?}"); - } - } - }, - ); - if let Err(why) = walked { - log!("xHCI: the capability list at PCI {:02x}:{:02x}.{} does not walk: {why:?}", - pci_dev.bus, pci_dev.dev, pci_dev.func); - } - let (usb2, usb3) = protocols.counts(max_ports); - log!("xHCI: {usb2} USB2 and {usb3} USB3 port register(s) of {max_ports} named, \ - {refused} capability(ies) refused"); - protocols -} - -fn init_one(pci_dev: &PciDevice) -> Option { - // The silicon beside the slot: a machine with two controllers is two - // different parts as often as it is one twice, and the slot alone does not - // say which. - log!( - "xHCI: found at PCI {:02x}:{:02x}.{} {:04x}:{:04x}", - pci_dev.bus, - pci_dev.dev, - pci_dev.func, - pci_dev.vendor_id(), - pci_dev.device_id() - ); - - // xHCI 1.2 §5.2.1 puts the capability registers in BAR 0; a controller - // that doesn't is one this driver cannot address. - let bar_addr = match pci_dev.memory_bar(0) { - Ok(memory) => memory.address(), - Err(why) => { - log!("xHCI: NOT INITIALISED at PCI {:02x}:{:02x}.{} — its capability registers are in \ - BAR 0 and {}", pci_dev.bus, pci_dev.dev, pci_dev.func, why); - return None; - } - }; - pci_dev.enable_bus_master(); - log!("xHCI: BAR0={:#x}", bar_addr); - - // Before reset and before the port scan: a controller whose interrupts - // can't be delivered must not reach either. - let Some(irq) = arm_interrupt(pci_dev) else { - log!( - "xHCI: NOT INITIALISED at PCI {:02x}:{:02x}.{} — the controller offers neither \ - MSI-X nor MSI, and this driver has no other way to be told it has anything to \ - say. No USB device on it can be used.", - pci_dev.bus, pci_dev.dev, pci_dev.func - ); - return None; - }; - log!("xHCI: {irq} enabled ({XHCI_VECTOR})"); - - let bar = crate::mm::paging::map_mmio(bar_addr, 0x10000, MmioPolicy::Uncacheable); - - let cap_length = bar.read_u8(CAP_CAPLENGTH) as u64; - let hcsparams1 = bar.read_u32(CAP_HCSPARAMS1); - let hcsparams2 = bar.read_u32(CAP_HCSPARAMS2); - let hccparams1 = bar.read_u32(CAP_HCCPARAMS1); - let db_offset = (bar.read_u32(CAP_DBOFF) & !0x3) as u64; - let rts_offset = (bar.read_u32(CAP_RTSOFF) & !0x1F) as u64; - - let max_slots = (hcsparams1 & 0xFF) as u8; - let max_ports = ((hcsparams1 >> 24) & 0xFF) as u8; - let csz = ((hccparams1 >> 2) & 1) != 0; - let context_size: usize = if csz { 64 } else { 32 }; - - // Refuses this controller by name rather than taking the machine down with it. - let refuse = |why: core::fmt::Arguments| { - log!("xHCI: NOT INITIALISED at PCI {:02x}:{:02x}.{} — {why}. No USB device on it can \ - be used.", pci_dev.bus, pci_dev.dev, pci_dev.func); - }; - - // checked_sub, not `-`: an offset outside the 64 KiB window would otherwise - // wrap to bar_size and pass Mmio::subregion's check, faulting on first use. - let bar_size = 0x10000u64; - let (Some(db_len), Some(rt_len)) = - (bar_size.checked_sub(db_offset), bar_size.checked_sub(rts_offset)) - else { - refuse(format_args!( - "DBOFF={db_offset:#x} RTSOFF={rts_offset:#x} put its registers outside the \ - {bar_size:#x} window this driver maps" - )); - return None; - }; - let op_base = bar.subregion(cap_length, bar_size - cap_length); - let db_base = bar.subregion(db_offset, db_len); - let rt_base = bar.subregion(rts_offset, rt_len); - - let pagesize = op_base.read_u32(OP_PAGESIZE) & 0xFFFF; - log!("xHCI: max_slots={} max_ports={} ctx_size={} pagesize={:#x}", - max_slots, max_ports, context_size, pagesize); - // Tests that 4 KiB is included, not that it's the only supported size; - // scratchpad buffer entries are one PAGE apart, so a controller without it - // would corrupt memory silently. - if pagesize & 1 == 0 { - refuse(format_args!( - "PAGESIZE={pagesize:#x} does not include 4 KiB, and every ring, context and \ - scratchpad buffer here is placed at 4 KiB" - )); - return None; - } - - let max_sp_hi = ((hcsparams2 >> 21) & 0x1F) as usize; - let max_sp_lo = ((hcsparams2 >> 27) & 0x1F) as usize; - let layout = Layout::new((max_sp_hi << 5) | max_sp_lo, max_slots); - log!("xHCI: dma {} KiB: scratchpad={} device blocks={} of {} B (max_slots={})", - layout.pool_size / 1024, layout.scratch_count, layout.dev_blocks, DEV_STRIDE, max_slots); - - // Before any other access: firmware may still own the controller for - // legacy keyboard emulation, and resetting under SMM is a fight with no diagnostic. - legacy::take_ownership(&bar, bar_size, hccparams1); - let protocols = read_protocols(&bar, bar_size, hccparams1, max_ports, pci_dev); - - let usbcmd = op_base.read_u32(OP_USBCMD); - if usbcmd & USBCMD_RS != 0 { - op_base.write_u32(OP_USBCMD, usbcmd & !USBCMD_RS); - } - let deadline_ms = USB_TIMEOUT_NS / 1_000_000; - if !settles(|| op_base.read_u32(OP_USBSTS) & USBSTS_HCH != 0) { - refuse(format_args!("it never halted, within {deadline_ms} ms of being asked to")); - return None; - } - - op_base.write_u32(OP_USBCMD, USBCMD_HCRST); - if !settles(|| op_base.read_u32(OP_USBCMD) & USBCMD_HCRST == 0) { - refuse(format_args!("it held HCRST for {deadline_ms} ms")); - return None; - } - if !settles(|| op_base.read_u32(OP_USBSTS) & USBSTS_CNR == 0) { - refuse(format_args!("it stayed Controller Not Ready for {deadline_ms} ms after its reset")); - return None; - } - log!("xHCI: controller reset"); - - // An address space of this controller's own, holding one pool and nothing - // else; attached before the first address is written to a register. - let space = crate::iommu::DeviceSpace::create(&[*pci_dev]); - // After the reset: a controller refused above never allocates, and - // `DmaPool` frees the pool — and takes it back out of the domain — on every - // refusal below when it drops. - // xHCI 1.2 §5.3.6: HCCPARAMS1's AC64 says whether it issues 64-bit addresses. - let limit = if hccparams1 & 1 != 0 { crate::iommu::Limit::WIDEST } else { crate::iommu::Limit::NARROWEST }; - let pool = match DmaPool::alloc_in(layout.pool_size, space, limit) { - Ok(pool) => pool, - Err(why) => { - refuse(format_args!("its pool cannot be placed where it reaches: {why}")); - return None; - } - }; - space.attach(pci_dev.bus, pci_dev.dev, pci_dev.func); - - // MaxSlotsEn caps what this driver can track; the controller then refuses - // Enable Slot past it. - op_base.write_u32(OP_CONFIG, layout.dev_blocks as u32); - - { - let dma = pool.view(); - dma.zero(); - - if layout.scratch_count > 0 { - for i in 0..layout.scratch_count { - let buf = dma.device_addr() + (layout.scratch_buffers + i * PAGE) as u64; - // Must run before DCBAA[0] is written below: that's what tells - // the controller the scratchpad array exists. - dma.write::(layout.scratch_array + i * core::mem::size_of::(), buf); - } - // DCBAA slot 0 is the scratchpad array pointer, not a device context. - super::super::write_dcbaa(dma, 0, dma.device_addr() + layout.scratch_array as u64); - log!("xHCI: {} scratchpad buffers configured", layout.scratch_count); - } - - op_base.write_u64(OP_DCBAAP, dma.device_addr() + OFF_DCBAA as u64); - - // CRCR bit 0 is RCS; the pointer is 64-byte aligned so `| 1` only sets - // that bit (xHCI 1.2 §5.4.5). - op_base.write_u64(OP_CRCR, (dma.device_addr() + OFF_CMD_RING as u64) | 1); - - // Must be written before IR0_ERSTBA below: that's what gives the - // controller the table's address. - dma.write::(OFF_ERST, ErstEntry { - ring_base: dma.device_addr() + OFF_EVT_RING as u64, - ring_size: RING_SIZE as u32, - _reserved: 0, - }); - rt_base.write_u32(IR0_ERSTSZ, 1); - rt_base.write_u64(IR0_ERDP, dma.device_addr() + OFF_EVT_RING as u64); - rt_base.write_u64(IR0_ERSTBA, dma.device_addr() + OFF_ERST as u64); - - rt_base.write_u32(IR0_IMOD, 0); - rt_base.write_u32(IR0_IMAN, 3); - - op_base.write_u32(OP_USBCMD, 1 | (1 << 2)); - } - if !settles(|| op_base.read_u32(OP_USBSTS) & 1 == 0) { - refuse(format_args!("it stayed halted for {deadline_ms} ms after R/S")); - return None; - } - log!("xHCI: controller started"); - - // No refusal after this point: `leak` gives up DmaPool's automatic free, so - // a None return past here would leak the DMA pool. - let dma = pool.leak(); - let cmd_ring = TrbRing::init(dma.subview(OFF_CMD_RING, PAGE)); - - // HCRST leaves this controller's ports unpowered when it has Port Power - // Control, and an unpowered port reports no device for the boot. - // - // PP is RW only with PPC; on a controller without it the write is a no-op - // and the readback afterward is what says so. - let mut powered = 0; - for p in 0..max_ports { - let off = OP_PORT_BASE + p as u64 * PORT_REG_SIZE; - let portsc = op_base.read_u32(off); - if portsc & PORTSC_PP == 0 { - op_base.write_u32(off, Portsc::from_raw(portsc).neutral().powered().raw()); - } - if op_base.read_u32(off) & PORTSC_PP != 0 { - powered += 1; - } - } - let powered_at = crate::clock::nanos_since_boot(); - log!("xHCI: {powered}/{max_ports} root-hub ports powered (PPC={})", - u8::from(hccparams1 & HCC_PPC != 0)); - - // Kept even with no HID bound: it is reset, started and armed, so dropping - // it would leave a live interrupter with nothing draining its event ring. - Some(XhciController { - pci: *pci_dev, - op_base, - db_base, - rt_base, - max_ports, - powered_at, - context_size, - layout, - pool: dma, - protocols, - cmd_ring, - event_ring: dma.subview(OFF_EVT_RING, PAGE), - event_head: 0, - event_phase: true, - devices: Vec::new(), - msc: [MscBlock::FREE; MSC_BLOCKS], - awaited: Vec::new(), - ports: (0..max_ports) - .map(|p| { - let mut port = PortState::EMPTY; - port.speaks(protocols.of(p)); - port - }) - .collect(), - ports_dirty: false, - outstanding: Outstanding::EMPTY, - after_break: toyos_xhci::call::AfterBreak::CLOSED, - bulk_began: 0, - stopped: None, - }) -} -/// Initialize and configure one USB device found connected at bring-up, -/// waiting for each step. -/// -/// The reset kind is one [`port::reset_needed`] read through -/// [`port::inherited_reset`] — every device here was left by whatever ran -/// before this kernel, so none is enumerated without a reset — and what a -/// completion meant is [`port::reset_outcome`]'s. -pub fn init_device(ctrl: &mut XhciController, port_idx: u8, protocol: Option) { - let needed = port::reset_needed(protocol, ctrl.read_portsc(port_idx)); - if needed.is_none() { - log!("xHCI: port {} link already trained before this kernel ran; warm resetting it \ - before its device is asked anything", port_idx + 1); - } - let mut kind = port::inherited_reset(needed); - reset_port(ctrl, port_idx, kind); - // At most two rounds: §4.19.5.1 has one escalation, hot to warm, and both - // failure shapes below leave `kind` warm, from which neither retries. - loop { - // Bounded: a port that never finishes its reset costs that port, not the boot. - if super::settles(|| reset_done(ctrl, port_idx)) { - match port::reset_outcome(kind, protocol, ctrl.read_portsc(port_idx)) { - ResetOutcome::Enumerate => return configure(ctrl, port_idx, Some(kind)), - ResetOutcome::Escalate(write) => { - log!("xHCI: port {} failed its hot reset (PORTSC {:#010x}); warm resetting it", - port_idx + 1, ctrl.read_portsc(port_idx).raw()); - ctrl.write_portsc(port_idx, write); - kind = Reset::Warm; - continue; - } - ResetOutcome::GaveUp(why) => { - match why { - GaveUp::LinkNeverTrained => log!( - "xHCI: port {} is SuperSpeed and its link would not train, warm \ - reset included (PORTSC {:#010x}); skipping it", - port_idx + 1, ctrl.read_portsc(port_idx).raw()), - GaveUp::ResetFailed => log!( - "xHCI: port {} completed its hot reset without enabling \ - (PORTSC {:#010x}); skipping it", - port_idx + 1, - ctrl.read_portsc(port_idx).raw()), - GaveUp::ResetNeverFinished => { - unreachable!("a completed reset cannot have never finished") - } - } - return ctrl.ports[usize::from(port_idx)].gave_up(why); - } - } - } - // xHCI 1.2 §4.19.1.2.4: a SuperSpeed link left Inactive by a failed hot - // reset needs a warm reset; without this the port is lost for the boot. - if kind == Reset::Hot && protocol == Some(Protocol::Usb3) { - log!("xHCI: port {} did not take a hot reset (link {:?}); warm resetting it", - port_idx + 1, ctrl.read_portsc(port_idx).link_state()); - reset_port(ctrl, port_idx, Reset::Warm); - kind = Reset::Warm; - continue; - } - log!("xHCI: port {} never finished its reset (PORTSC {:#010x}); skipping it", - port_idx + 1, ctrl.read_portsc(port_idx).raw()); - return ctrl.ports[usize::from(port_idx)].gave_up(GaveUp::never_finished(kind)); - } -} - -/// Everything between a port that has just finished its reset and a device the -/// driver can use, run to the end in place. -pub fn configure(ctrl: &mut XhciController, port_idx: u8, after: Option) { - begin(ctrl, port_idx, after); - ctrl.settle_outstanding(); -} -/// Scans every port on the controller and initializes each connected device. -/// -/// Serial by construction: the input context, EP0 ring and descriptor buffer -/// are reused across ports. -pub fn scan_ports(ctrl: &mut XhciController) { - for p in 0..ctrl.max_ports { - // No speed printed here: xHCI 1.2 §4.19.5 says Port Speed isn't valid - // until PR transitions 1→0, so QEMU's early value would read as fact - // and isn't. - if ctrl.read_portsc(p).connected() { - log!("xHCI: port {} connected", p + 1); - init_device(ctrl, p, ctrl.protocols.of(p)); - } - } - // Must run: a change flag raises an event only on 0→1, so a CSC left set - // from boot-time attach would make the first unplug after boot go unreported. - ctrl.acknowledge_port_changes(); - // Completions from an earlier port's device can arrive during a later - // port's enumeration; without this drain a broken one goes unrecorded. - ctrl.settle_outstanding(); -} - diff --git a/kernel/src/drivers/xhci/wait/mod.rs b/kernel/src/drivers/xhci/wait/mod.rs deleted file mode 100644 index 07268843310..00000000000 --- a/kernel/src/drivers/xhci/wait/mod.rs +++ /dev/null @@ -1,455 +0,0 @@ -//! Everything in this driver that waits, in the three contexts where it is -//! correct: [`boot`]'s scan (no scheduler pass yet exists), [`msc`]'s disk -//! reads/writes (the faulting thread spends its own time), and -//! [`msc::bind`]'s bring-up (the one blocking call a scheduler pass may still -//! reach). `wait_command`, `wait_transfer`, `settles`, `run_command`, -//! `control_transfer`, `restart_endpoint` and `settle_outstanding` are private -//! to this module, so `xhci`, `xhci::device` and `xhci::hid` cannot reach them -//! from a scheduler pass. - -pub mod boot; -pub mod msc; - -/// Deepest preempt depth seen while waiting for a disk transfer; a -/// measurement only. -#[cfg(feature = "boot-actuators")] -mod depth_probe { - -} - -use crate::log; -use super::{deadline, enqueue_control, log_unrecoverable, Completion, Trb, TrbRing}; -use super::{XhciController, EVENT_TRANSFER, EVENT_CMD_COMPLETE, USB_TIMEOUT_NS}; -use toyos_xhci::call::NotTaken; -use toyos_xhci::job::{self, Await, CC_SHORT_PACKET, CC_STALL, CC_SUCCESS}; -use toyos_xhci::recovery::{Act, NeedsConfigure, Recovery}; -use toyos_xhci::scan; - -/// How one control transfer ended; `Done` carries the bytes actually moved, -/// since the completion code alone cannot say. -#[derive(Clone, Copy)] -enum Control { - /// Both stages completed; `delivered` is zero for a transfer with no data stage. - Done { delivered: u16 }, - /// The controller reported `code` for the named stage. - Failed { stage: &'static str, code: u32 }, - /// Nothing came back for the named stage, for [`Quiet`]'s reason. - Silent { stage: &'static str, why: Quiet }, - /// Not put on the ring: the call it belongs to may spend nothing more. - NotSent(NotTaken), -} - -/// Why a wait ended with no event; only [`Quiet::Elapsed`] spent the timeout budget. -#[derive(Clone, Copy, PartialEq, Eq)] -pub(super) enum Quiet { - /// [`USB_TIMEOUT_NS`] passed with the port still connected. - Elapsed, - /// The port reads disconnected. - Gone, - /// What this part of a call whose transport broke may spend ran out before this wait's own timeout did. - Spent, - /// A staged break skipped the wait by design. - #[cfg(feature = "boot-actuators")] - Staged, -} - -impl Quiet { - /// What to say about a step that ended this way; `step` is the caller's word for it. - pub(super) fn about( - self, - step: &str, - kind: &str, - f: &mut core::fmt::Formatter<'_>, - ) -> core::fmt::Result { - match self { - Self::Elapsed => write!( - f, - "no answer in the {step} {kind} in {} ms", - USB_TIMEOUT_NS / 1_000_000 - ), - Self::Gone => write!(f, "the port disconnected during the {step} {kind}"), - Self::Spent => write!( - f, - "the bound on this part of the call ran out during the {step} {kind}" - ), - #[cfg(feature = "boot-actuators")] - Self::Staged => write!(f, "a staged break skipped the {step} {kind} wait"), - } - } -} - -impl Control { - /// Whether the transfer completed. - fn done(self) -> bool { - matches!(self, Self::Done { .. }) - } -} - -impl core::fmt::Display for Control { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Done { delivered } => write!(f, "{delivered} B delivered"), - Self::Failed { stage, code } => write!(f, "{stage} stage completion {}", Completion(*code)), - Self::Silent { stage, why } => why.about(stage, "stage", f), - Self::NotSent(why) => write!(f, "not sent: {why}"), - } - } -} - -/// One endpoint, as [`XhciController::restart_endpoint`] needs to see it. -struct Restart<'a> { - slot_id: u8, - /// The device block whose output context carries this endpoint's state. - ctx_block: usize, - dci: u8, - /// The address the *device* knows this endpoint by, for CLEAR_FEATURE. - ep_addr: u8, - /// Where in the pool the transfer ring lives; recovery rebuilds it rather - /// than resuming a ring the controller has a stale dequeue pointer into. - ring_at: usize, - ring: &'a mut TrbRing, - ep0_ring: &'a mut TrbRing, -} - -/// Spin until `ready`, and say whether that happened inside [`USB_TIMEOUT_NS`]. -fn settles(ready: impl Fn() -> bool) -> bool { - crate::clock::settles(USB_TIMEOUT_NS, ready) -} - -/// When a wait on the event ring gives up (`toyos_xhci::late`), against this -/// kernel's clock and this driver's ring. -struct Late(toyos_xhci::late::Late); - -impl Late { - fn new(deadline: u64) -> Self { - Self(toyos_xhci::late::Late::new(deadline, super::RING_SIZE)) - } - - fn past(&self) -> bool { - self.0.past(crate::clock::nanos_since_boot()) - } - - fn gives_up(&mut self) -> bool { - self.0.gives_up(crate::clock::nanos_since_boot()) - } -} - -impl XhciController { - /// Now, and when a wait starting now gives up: on its own timeout, or where this part of a call whose transport broke ends (`toyos_xhci::call`). - fn wait_ends(&self) -> (u64, u64) { - let now = crate::clock::nanos_since_boot(); - (now, self.after_break.wait_ends(now, USB_TIMEOUT_NS)) - } - - /// [`settles`], inside what the call may still spend. - fn settles_within_call(&self, ready: impl Fn() -> bool) -> bool { - let now = crate::clock::nanos_since_boot(); - crate::clock::settles(self.after_break.wait_left(now, USB_TIMEOUT_NS), ready) - } - - /// Take one endpoint back to a state that runs TRBs, waiting for each step. - fn restart_endpoint(&mut self, ep: Restart<'_>) -> bool { - let halt = Some((ep.ep_addr, ep.ep0_ring)); - self.run_recovery(ep.slot_id, ep.dci, ep.ctx_block, ep.ring, ep.ring_at, halt) - } - - /// [`Recovery`] run to its end, one blocking command at a time. `halt` is - /// the address the device knows the endpoint by and the EP0 ring a - /// CLEAR_FEATURE for it goes out on, or `None` for an endpoint with no Halt - /// feature to clear. - #[allow(clippy::too_many_arguments)] - fn run_recovery( - &mut self, - slot_id: u8, - dci: u8, - ctx_block: usize, - ring: &mut TrbRing, - ring_at: usize, - halt: Option<(u8, &mut TrbRing)>, - ) -> bool { - let slot = self.slot(slot_id); - let state = self.endpoint_state(ctx_block, dci); - log!("xHCI: {slot} endpoint {dci} is {state}, recovering"); - let (mut seq, mut act) = match Recovery::begin(state) { - Ok(begun) => begun, - Err(NeedsConfigure(state)) => { - log_unrecoverable(slot, dci, state); - return false; - } - }; - loop { - let cmd = match act { - Act::Running => return true, - // The last act of every route that has one. - Act::ClearHalt => { - return match halt { - Some((ep_addr, ep0_ring)) => { - self.clear_endpoint_halt(slot_id, ctx_block, ep0_ring, ep_addr) - } - None => true, - } - } - Act::Command(cmd) => cmd, - }; - let trb = self.recovery_trb(cmd, slot_id, dci, ring, ring_at); - if !self.run_command(trb, cmd.name()) { - return false; - } - act = seq.completed(); - } - } - - /// The default control pipe, back to a state that runs TRBs. Its own - /// entry point: USB 2.0 §9.4.5 defines no Halt feature for the default - /// pipe, so only the controller's half (xHCI 1.2 §4.6.8) applies. - fn restart_control_endpoint( - &mut self, - slot_id: u8, - ctx_block: usize, - ring: &mut TrbRing, - ) -> bool { - self.run_recovery( - slot_id, - super::EP0_DCI, - ctx_block, - ring, - ctx_block + super::DEV_EP0_RING, - None, - ) - } - - /// CLEAR_FEATURE(ENDPOINT_HALT) for the endpoint the device knows as - /// `ep_addr`: the half of a recovery the **device** answers. - fn clear_endpoint_halt( - &mut self, - slot_id: u8, - ctx_block: usize, - ep0_ring: &mut TrbRing, - ep_addr: u8, - ) -> bool { - let cleared = self - .control_transfer(slot_id, ctx_block, ep0_ring, 0x02, 0x01, 0, ep_addr as u16, None, 0); - if !cleared.done() { - log!("xHCI: {} would not clear the halt on endpoint {ep_addr:#04x}: {cleared}", - self.slot(slot_id)); - return false; - } - true - } - - /// Run whatever is outstanding to its end, waiting for each answer; the - /// boot scan only, before `init` publishes the controller for anything else to poll. - /// - /// **The slot is free when this returns**, which is the contract the scan's - /// next port rests on: `device::begin` submits its Enable Slot into the one - /// slot without asking, as the runtime path cannot. What bounds the wait is - /// the operation's own deadline, taken by `advance_outstanding`, and not - /// the silence bound below — a bound the blocking bind inside - /// `advance_outstanding` has usually spent before the refusal at the end of - /// it submits anything. [`toyos_xhci::scan`] is the decision. - fn settle_outstanding(&mut self) { - // **Bounded on silence and not on work**, and spendable only on the - // broken-endpoint half: `broke_with` clears when a recovery takes, a - // device can break again as soon as it has, and nothing in that says - // when to stop. A scan that is still being answered is not stuck, so - // every event the controller produces re-arms it. - let mut quiet_until = deadline(); - loop { - if self.drain_events() { - quiet_until = deadline(); - } - // After the drain, never inside it: everything below submits. - self.advance_outstanding(); - // Also asks about `broke_with`: a halted endpoint raises no further - // interrupt, so a first-transfer failure would otherwise go - // unrecovered for the rest of boot. - let broken = self.devices.iter().any(|d| d.broke_with.is_some()); - let now = crate::clock::nanos_since_boot(); - match scan::settle(self.outstanding.busy(), broken, now, quiet_until) { - scan::Settle::Done => return, - scan::Settle::GaveUp => { - log!("xHCI: the boot scan heard nothing for {} ms with an endpoint still \ - broken; the rest of this boot goes on without it", - USB_TIMEOUT_NS / 1_000_000); - return; - } - scan::Settle::Recover => self.recover_endpoints(), - scan::Settle::Wait => {} - } - core::hint::spin_loop(); - } - } - - /// The completion code and slot id of the command that was enqueued at - /// `trb`, or `None` if the controller never answered. Matched by the TRB - /// address, not the next completion event, per Command Completion Event's - /// own addressing (xHCI 1.2 §6.4.2.2). - fn wait_command(&mut self, trb: u64) -> Option<(u32, u32)> { - let (_, deadline) = self.wait_ends(); - let mut late = Late::new(deadline); - loop { - if late.gives_up() { - return None; - } - let Some(event) = self.next_event() else { - if late.past() { - return None; - } - core::hint::spin_loop(); - continue; - }; - if (event.control >> 10) & 0x3F == EVENT_CMD_COMPLETE && event.param & !0xF == trb { - return Some(((event.status >> 24) & 0xFF, (event.control >> 24) & 0xFF)); - } - self.dispatch_event(event); - } - } - - /// Submit `trb` and say whether the controller accepted it, logging - /// anything it did not under `what`'s name. - fn run_command(&mut self, trb: Trb, what: &str) -> bool { - match self.command_code(trb, what) { - Some(CC_SUCCESS) => true, - Some(code) => { - log!("xHCI: {what} failed: {}", Completion(code)); - false - } - None => false, - } - } - - /// Submit `trb` and hand back the completion code, for a caller with its - /// own answer to one; `None` is a controller that never answered, which is - /// logged here. - fn command_code(&mut self, trb: Trb, what: &str) -> Option { - let began = crate::clock::nanos_since_boot(); - if let Err(why) = self.after_break.command(began) { - log!("xHCI: {what} not issued: {why}"); - return None; - } - let at = self.submit_command(trb); - match self.wait_command(at) { - Some((code, _)) => Some(code), - None => { - self.after_break.unanswered(); - // The controller's own word beside the silence: a command ring - // that stopped (CRCR.CRR clear) or a Host Controller Error - // (USBSTS.HCE) is a controller no further command reaches. - log!( - "xHCI: {what} timed out after {} ms with USBSTS={:#010x} and CRCR.CRR={}", - (crate::clock::nanos_since_boot() - began) / 1_000_000, - self.op_base.read_u32(super::OP_USBSTS), - (self.op_base.read_u64(super::OP_CRCR) >> 3) & 1 - ); - None - } - } - } - - /// The completion of the transfer queued at `trb` on (`slot`, `dci`), as a - /// completion code and the number of bytes the controller did *not* move. - /// Matched by (slot, dci, trb) rather than the endpoint, since a stalled - /// endpoint still completes late transfers this driver stopped waiting for. - fn wait_transfer(&mut self, slot: u8, dci: u8, trb: u64) -> Result<(u32, u32), Quiet> { - let on = Await::Transfer { slot, dci, trb }; - let (began, deadline) = self.wait_ends(); - let port = self.port_of_slot(slot); - let mut late = Late::new(deadline); - let quiet = |call: &toyos_xhci::call::AfterBreak| { - let cut = call.cut(began, crate::clock::nanos_since_boot(), USB_TIMEOUT_NS); - if cut { Quiet::Spent } else { Quiet::Elapsed } - }; - loop { - if late.gives_up() { - return Err(quiet(&self.after_break)); - } - let Some(event) = self.next_event() else { - if late.past() { - return Err(quiet(&self.after_break)); - } - // An unplugged device is not a slow one; the timeout budget is - // for a port that might still answer. - if port.is_some_and(|p| !self.read_portsc(p).connected()) { - return Err(Quiet::Gone); - } - core::hint::spin_loop(); - continue; - }; - let trb_type = (event.control >> 10) & 0x3F; - let answers = Await::Transfer { - slot: ((event.control >> 24) & 0xFF) as u8, - dci: ((event.control >> 16) & 0x1F) as u8, - trb: event.param & !0xF, - }; - if trb_type == EVENT_TRANSFER && answers == on { - return Ok(((event.status >> 24) & 0xFF, event.status & 0x00FF_FFFF)); - } - // Not this wait's event: forwarded so a bound device's own - // interrupt ring stays fed rather than dropped here. - self.dispatch_event(event); - } - } - - /// One control transfer on `ring`, which must be the EP0 ring named by - /// `slot`'s device context. - // Nine arguments: a USB setup packet has five fields, which is what a - // caller varies (xHCI 1.2 §6.4.1.2.1). - #[allow(clippy::too_many_arguments)] - fn control_transfer( - &mut self, - slot: u8, - ctx_block: usize, - ring: &mut TrbRing, - bm_request_type: u8, - b_request: u8, - w_value: u16, - w_index: u16, - data_buf: Option, - data_len: u16, - ) -> Control { - if let Err(why) = self.after_break.request(crate::clock::nanos_since_boot()) { - return Control::NotSent(why); - } - let trbs = enqueue_control( - ring, bm_request_type, b_request, w_value, w_index, data_buf, data_len, - ); - self.ring_doorbell(slot, 1); - - let mut delivered = 0u16; - if let Some(data) = trbs.data { - match self.wait_transfer(slot, 1, data) { - Ok((CC_SUCCESS | CC_SHORT_PACKET, residue)) => { - // No more than `data_len`, so it is a `u16`. - delivered = job::moved(data_len.into(), residue) as u16; - } - // An errored data stage halts EP0, so the status stage's TRB - // never runs; it is not waited for. - Ok((code, _)) => { - self.recover_after(slot, ctx_block, ring, code); - return Control::Failed { stage: "data", code }; - } - Err(why) => return Control::Silent { stage: "data", why }, - } - } - match self.wait_transfer(slot, 1, trbs.status) { - Ok((CC_SUCCESS, _)) => Control::Done { delivered }, - Ok((code, _)) => { - self.recover_after(slot, ctx_block, ring, code); - Control::Failed { stage: "status", code } - } - Err(why) => Control::Silent { stage: "status", why }, - } - } - - /// Take EP0 back out of Halted where `code` says the device stalled the - /// transfer, before the failure reaches a caller likely to send another one. - fn recover_after(&mut self, slot: u8, ctx_block: usize, ring: &mut TrbRing, code: u32) { - if code != CC_STALL { - return; - } - if !self.restart_control_endpoint(slot, ctx_block, ring) { - log!("xHCI: {} EP0 stayed halted after the stall", self.slot(slot)); - } - } - -} diff --git a/kernel/src/drivers/xhci/wait/msc.rs b/kernel/src/drivers/xhci/wait/msc.rs deleted file mode 100644 index a1134debeac..00000000000 --- a/kernel/src/drivers/xhci/wait/msc.rs +++ /dev/null @@ -1,1775 +0,0 @@ -//! USB Mass Storage, Bulk-Only Transport with SCSI (interface class 0x08, -//! subclass 0x06, protocol 0x50) only — no UAS, no CBI/CB, one logical unit, -//! no removable-media handling, no MODE SENSE. One command in flight per -//! controller: `with_disk` holds the controller lock for the whole of it. -//! Everything here comes off the wire and is checked, never trusted; refusal -//! is by name, never a panic. - -use crate::mm::Dma; - -use crate::block::{BlockError, BlockResult}; -use crate::log; -use crate::scheduler::Operation; -use crate::time::{Budget, Deadline, Duration}; -use super::{Control, Quiet, Restart}; -use super::super::{log_unrecoverable, Completion, Disk, StorageGeometry, Trb}; -use super::super::{look_for, ports_wanted, with_disk_by, Whereabouts}; -use super::super::{TrbRing, XhciController, PAGE, TRB_ADDRESS_DEVICE, TRB_CONFIGURE_EP, TRB_RESET_DEVICE}; -use super::super::{stop, TRB_NORMAL, OFF_INPUT_CTX}; -use super::super::{AFTER_BREAK, CC_CONTEXT_STATE_ERROR, EP0_DCI}; -use super::super::{MSC_IN_RING, MSC_OUT_RING, MSC_CBW, MSC_CSW, MSC_SCRATCH, MSC_SCRATCH_LEN}; -use super::super::MSC_INPUT_CTX; -use super::super::{MSC_DATA, MSC_DATA_LEN, MSC_MAX_BLOCKS, MSC_STRIDE}; -use super::super::device::Endpoint; -use toyos_xhci::bot::{self, Bot, Phase, RoundTrip, CBW_LEN, CSW_LEN}; -use toyos_xhci::call::{AfterBreak, NotIssued}; -use toyos_xhci::configure::{self, BulkEndpoint}; -use toyos_xhci::flush::Debt; -use toyos_xhci::identity::{self, Identity, Serial, UsbId}; -use toyos_xhci::job::CC_SUCCESS; -use toyos_xhci::ladder::{self, AfterReset, AfterRung, Left, PortStep, Run, Rung}; -use toyos_xhci::port; -use toyos_xhci::reset_recovery::{self, Answered, GaveUp, Look, Pipe, Quiescing, SlotGoes, Step}; -use toyos_scsi::{self as scsi, BringUp, Cdb, Fail, Flushed, Geometry, Heard, Moved, Printable, Reply}; -use toyos_scsi::{Refusal, Sense, Transfer, HOST_BLOCK}; - -/// A region, not an address: the CBW's length is the region's own size, so -/// no command can name a length its destination lacks. -type DataPhase = Option>; - -/// Why a round trip broke, with this driver's reason for a silence. -type Broke = bot::Broke; - -/// How a rung ended when it did not verify; a rung that did is `Ok(())`. -type Unverified = ladder::Unverified; - -/// Wall-clock budget on bring-up's ready attempts: bounds when [`bring_up`] -/// stops *starting* attempts, not the one already running. -const READY_BUDGET: Budget = Budget::of( - Duration::from_millis(500), - "the device is reported as not becoming ready and the boot goes on without it", -); - -/// The most breaks a device's transport gets in a row before the device is -/// offline: one per rung of `toyos_xhci::ladder`. -pub(in crate::drivers::xhci) const MAX_TRANSPORT_BREAKS: u8 = ladder::MOST_BREAKS; - -/// What the configuration descriptor said about a mass-storage interface; -/// both endpoints, always, each valid because `Endpoint` only comes from -/// its own constructor. -#[derive(Clone, Copy)] -pub struct MscInterface { - pub iface_num: u8, - pub in_ep: Endpoint, - pub out_ep: Endpoint, -} - -/// One bound disk; `Copy` so a command can borrow the controller and the -/// device's own rings at once without the controller borrowing itself. -#[derive(Clone, Copy)] -pub struct MscDevice { - slot_id: u8, - /// The interface and its two bulk endpoints as the descriptor gave them, - /// kept because a Reset Recovery re-creates both endpoints and has to - /// describe them to the controller exactly as the bind did. - pair: MscInterface, - /// The root-hub port the device is on, known from the first command of its - /// bind — before the port itself is told which slot it holds. - port_idx: u8, - /// What the enumeration addressed and configured the device with, which - /// the port rung addresses and configures it with again: the speed its port - /// trained at, the control endpoint's packet size, and the configuration - /// value. - enumerated: Enumerated, - /// Byte offset of this device's block in its controller's DMA pool. - block: usize, - /// Byte offset of the device context block; its endpoint states decide - /// which recovery command is legal. - dev_block: usize, - ep0_ring: TrbRing, - in_ring: TrbRing, - out_ring: TrbRing, - tag: u32, - /// Zero until bring-up reads the disk's size. - geometry: Geometry, - run: Run, - /// Set once the device was taken offline; the device is not spoken to again. - failed: bool, - /// Where [`XhciController::take_offline`] said the slot goes, until - /// somebody has acted on it; see [`Self::take_slot_owed`]. - slot_goes: Option, - /// Set once the device refuses SYNCHRONIZE CACHE; logged once, not per - /// flush — a log line would itself be pending content the next flush drains. - no_write_cache: bool, - /// The flush this instance owes for its own writes, and how many devices - /// serving this disk left owing one ([`Self::owes_a_flush`]). - debt: Debt, - /// What the device says it is, which a device that binds after this one - /// left must match to take its number. - identity: Identity, - /// When this driver last reset the device's port. - reset_at: Option, - /// The device's port was found empty inside a call; see - /// [`XhciController::hold_for_return`]. - left: bool, -} - -impl MscDevice { - /// Whether this device has answered SYNCHRONIZE CACHE with INVALID COMMAND - /// OPERATION CODE, which is what makes a flush's `Ok(())` mean "there was - /// nothing to make durable" rather than "a cache was emptied". - pub(in crate::drivers::xhci) fn refused_flush(&self) -> bool { - self.no_write_cache - } - - /// The port whose slot is owed back to the controller, once: the disk was - /// taken offline with its endpoints Stopped, and its slot is still enabled. - pub(in crate::drivers::xhci) fn take_slot_owed(&mut self) -> Option { - if self.slot_goes != Some(SlotGoes::Back) { - return None; - } - self.slot_goes = None; - Some(self.port_idx) - } - - /// Until when the disk is held for its device to come back, seen gone at - /// `now`: only after a reset of this driver's (`toyos_xhci::identity`). - pub(in crate::drivers::xhci) fn returns_by(&self, now: u64) -> Option { - identity::returns_by(self.reset_at, now) - } - - pub(in crate::drivers::xhci) fn identity(&self) -> &Identity { - &self.identity - } - - /// Whether a device that left now could take writes reported complete with - /// it: held in a volatile cache no flush has emptied since - /// (`toyos_xhci::flush`). - pub(in crate::drivers::xhci) fn owes_a_flush(&self) -> bool { - self.debt.owed(self.no_write_cache) - } - - /// The disk's loss count this device hands the one that takes the disk - /// back, were it to leave now. - pub(in crate::drivers::xhci) fn losses_left(&self) -> u64 { - self.debt.left(self.no_write_cache) - } - - /// The device reported a transfer complete, whole or in part; `write` is - /// whether it was one. - fn wrote(&mut self, write: bool) { - if write { - self.debt.wrote(); - } - } - - pub(in crate::drivers::xhci) fn port_idx(&self) -> u8 { - self.port_idx - } - - /// Where the disk is and what it says it is: its root-hub port, the speed - /// that port trained at, and its device descriptor's ids. - pub fn inventory(&self) -> (u8, toyos_abi::inventory::UsbSpeed, toyos_xhci::identity::UsbId) { - (self.port_idx, self.enumerated.usb_speed, self.identity.usb) - } - - pub fn geometry(&self) -> StorageGeometry { - StorageGeometry { - logical_block_bytes: self.geometry.sector_bytes(), - blocks: self.geometry.blocks(), - } - } - - fn next_tag(&mut self) -> u32 { - self.tag = self.tag.wrapping_add(1); - self.tag - } - - fn in_dci(&self) -> u8 { - self.pair.in_ep.dci() - } - - fn out_dci(&self) -> u8 { - self.pair.out_ep.dci() - } - - /// One bulk endpoint as the controller names it. - fn dci(&self, pipe: Pipe) -> u8 { - match pipe { - Pipe::In => self.in_dci(), - Pipe::Out => self.out_dci(), - } - } - - /// The same endpoint as the *device* names it, for CLEAR_FEATURE. - fn ep_addr(&self, pipe: Pipe) -> u8 { - match pipe { - Pipe::In => self.pair.in_ep.addr, - Pipe::Out => self.pair.out_ep.addr, - } - } - - /// One bulk endpoint's ring and where in the pool it lives. - fn ring_mut(&mut self, pipe: Pipe) -> (&mut TrbRing, usize) { - match pipe { - Pipe::In => (&mut self.in_ring, self.block + MSC_IN_RING), - Pipe::Out => (&mut self.out_ring, self.block + MSC_OUT_RING), - } - } -} - -/// What [`MscDevice::enumerated`] holds. -#[derive(Clone, Copy)] -pub(in crate::drivers::xhci) struct Enumerated { - pub speed: u8, - /// `speed` decoded once, at bind, where a psiv with no default Protocol - /// Speed ID already refused the port. - pub usb_speed: toyos_abi::inventory::UsbSpeed, - pub ep0_packet: u16, - pub configuration: u8, -} - -/// A break as its line says it. -struct Told<'a>(&'a Broke); - -impl core::fmt::Display for Told<'_> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self.0 { - Broke::Code { phase, code, .. } => { - write!(f, "{phase} phase completion {}", Completion(*code)) - } - Broke::Silence { phase, why } => why.about(phase.named(), "phase", f), - Broke::Gone { phase } => Quiet::Gone.about(phase.named(), "phase", f), - Broke::Short { phase, moved, wanted } => { - write!(f, "{phase} phase moved {moved} of {wanted} B") - } - Broke::Stall { phase } => { - write!(f, "the {phase} phase stalled and the endpoint reset did not clear it") - } - Broke::PhaseError => f.write_str("the device reported a phase error"), - Broke::Reserved { status } => { - write!(f, "the CSW carries status {status:#04x}, which the class reserves") - } - Broke::Csw { what, got, want, status, residue } => write!( - f, - "CSW {what} {got:#x}, not {want:#x} (status {status}, {residue} B unmoved)" - ), - Broke::Residue { unmoved, of } => { - write!(f, "CSW claims {unmoved} B unmoved of {of}") - } - } - } -} - -/// A rung that did not bring its device back in step, as its line says it. -struct Ended<'a>(Rung, &'a Unverified); - -impl core::fmt::Display for Ended<'_> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self.1 { - Unverified::OutOfStep(why) => { - write!(f, "transport broke on the {}'s TEST UNIT READY: {}", self.0.named(), Told(why)) - } - Unverified::Failed => write!(f, "the {} was not answered", self.0.named()), - } - } -} - -/// Abandon one bulk transfer without waiting, once per boot, on the first -/// WRITE(10): only the wait is skipped, so recovery runs against a real -/// endpoint state — staged since nothing on the host side leaves one in flight. -#[cfg(feature = "boot-actuators")] -mod transport_break { - use core::sync::atomic::{AtomicBool, Ordering}; - - static UNSPENT: AtomicBool = AtomicBool::new(true); - static ARMED: AtomicBool = AtomicBool::new(false); - - /// Called where the driver is about to run a WRITE(10) data phase. - pub fn arm() { - if !crate::actuator::usb_transport_break() { - return; - } - ARMED.store(UNSPENT.swap(false, Ordering::Relaxed), Ordering::Relaxed); - } - - /// Called after the doorbell, where the wait would otherwise begin. - pub fn take() -> bool { - ARMED.swap(false, Ordering::Relaxed) - } -} - -/// What the port rung's reset is made for, in its line. -const RECOVERING: &str = "recovering"; - -/// The one line a device's refusal produces, wherever it is noticed — one -/// function so per-caller wording never obscures what the device said. -fn log_refusal(cdb: &Cdb, sense: Sense) { - log!("usb-storage: SCSI {:#04x} failed, sense {sense}", cdb.opcode()); -} - -/// A bulk transfer's completion, as the round trip hears it. -fn completed(completion: Result<(u32, u32), Quiet>) -> bot::Answer { - match completion { - Ok((code, residue)) => bot::Answer::Moved { code, residue }, - Err(Quiet::Gone) => bot::Answer::Gone, - Err(why) => bot::Answer::Silent(why), - } -} - -/// What a caller above the disk is told. -fn block_error(fail: Fail) -> BlockError { - match fail { - Fail::Device => BlockError::Device, - Fail::Budget => BlockError::BudgetExpired, - } -} - -/// Snapshot of `dev.block`'s address and value at the top of one round -/// trip, to catch a write to `MscDevice` from outside the driver while a -/// phase is waiting. -#[cfg(feature = "stack-witness")] -#[derive(Clone, Copy)] -struct BlockWitness { - at: u64, - was: usize, -} - -#[cfg(feature = "stack-witness")] -fn block_witness(dev: &MscDevice) -> BlockWitness { - BlockWitness { at: &raw const dev.block as u64, was: dev.block } -} - -/// Nothing in this driver writes `block` after `bind` hands the device -/// over, so any difference here is a write from outside it. -#[cfg(feature = "stack-witness")] -fn block_witness_holds(dev: &MscDevice, entered: BlockWitness) { - let at = &raw const dev.block as u64; - if at == entered.at && dev.block == entered.was { - return; - } - // SAFETY: driver code runs on the CPU whose GS base is its own `PerCpu`. - let (top, _) = unsafe { crate::arch::percpu::entry_stacks() }; - panic!( - "USB BOT WITNESS: MscDevice::block changed inside one round trip — the field at \ - {at:#018x} held {:#018x} and now holds {:#018x} (the frame moved by {}). This CPU's \ - Ring 3 entry stack is {top:#018x}, so the field stands {} bytes below it and the \ - running stack pointer is {:#018x}. `with_storage` copies the device onto this stack, so a \ - kernel text value here is a return address something else pushed.", - entered.was, - dev.block, - at.wrapping_sub(entered.at) as i64, - top.wrapping_sub(at) as i64, - crate::arch::cpu::stack_pointer(), - ); -} - -/// Which way a block transfer moves, so one loop serves both without a -/// `&[u8]` pretending to be a `&mut [u8]`. -enum Host<'a> { - Into(&'a mut [u8]), - From(&'a [u8]), -} - -impl Host<'_> { - fn len(&self) -> usize { - match self { - Self::Into(b) => b.len(), - Self::From(b) => b.len(), - } - } -} - -impl XhciController { - /// Run `f` against the disk at `at`, writing state back regardless of - /// outcome; `None` if no disk is there. - fn with_storage( - &mut self, - at: usize, - f: impl FnOnce(&mut Self, &mut Disk) -> R, - ) -> Option { - let mut disk = self.msc.get(at)?.disk?; - let out = f(self, &mut disk); - self.msc[at].disk = Some(disk); - if disk.dev.left { - self.hold_for_return(at, "its port read empty"); - } - Some(out) - } - - /// One of three operation entry points: recovers the caller's deadline - /// via [`Operation::deadline`], never done again below this call. - pub(super) fn msc_read(&mut self, at: usize, lba: u64, count: u32, buf: &mut [u8]) -> BlockResult { - let until = Operation::deadline(); - self.with_storage(at, |ctrl, disk| { - ctrl.transfer_blocks(&mut disk.dev, lba, count, Host::Into(buf), until) - }) - // No disk under this index is a device fact, never a budget. - .unwrap_or(Err(BlockError::Device)) - } - - pub(super) fn msc_write(&mut self, at: usize, lba: u64, count: u32, buf: &[u8]) -> BlockResult { - let until = Operation::deadline(); - self.with_storage(at, |ctrl, disk| { - ctrl.transfer_blocks(&mut disk.dev, lba, count, Host::From(buf), until) - }) - .unwrap_or(Err(BlockError::Device)) - } - - pub(super) fn msc_flush(&mut self, at: usize) -> BlockResult { - let until = Operation::deadline(); - self.with_storage(at, |ctrl, disk| { - let number = disk.index; - let dev = &mut disk.dev; - if dev.failed { - return Err(BlockError::Device); - } - // **The latch decides before the command is built, not after it is - // refused.** A device that has answered INVALID COMMAND OPERATION - // CODE once answers it every time, and issuing anyway costs a full - // Bulk-Only round trip plus a REQUEST SENSE — under `XHCI`, with - // preemption off, on the device the log is going to — for every - // `logkeeper` batch on a stick like the T14's. - if dev.no_write_cache { - return Ok(()); - } - let cdb = Cdb::SYNCHRONIZE_CACHE; - match scsi::flushed(ctrl.scsi(dev, &cdb, None, until)) { - Flushed::NoCache => { - dev.no_write_cache = true; - log!("usb-storage: disk {number} does not implement SYNCHRONIZE CACHE \ - (sense 0x05/0x20/0x00); its writes are durable once they complete"); - Ok(()) - } - Flushed::Emptied => { - dev.debt.flushed(); - Ok(()) - } - Flushed::Refused(sense) => { - log_refusal(&cdb, sense); - Err(BlockError::Device) - } - // Unlogged: `scsi` already named a budget, and a line here - // would itself be the next flush. - Flushed::Ended(fail) => Err(block_error(fail)), - } - }) - .unwrap_or(Err(BlockError::Device)) - } - - /// Move `count` 4 KiB blocks; `until` bounds the whole call, not one - /// command — [`Self::scsi`] refuses to start a command past the deadline. - fn transfer_blocks( - &mut self, - dev: &mut MscDevice, - lba: u64, - count: u32, - mut host: Host<'_>, - until: Deadline, - ) -> BlockResult { - let write = matches!(host, Host::From(_)); - // Caller/trait mismatch is a kernel bug: fail-fast. Below this line - // every check is about device numbers and gets a refusal instead. - assert_eq!(host.len(), count as usize * HOST_BLOCK as usize); - if dev.failed { - return Err(BlockError::Device); - } - let mut transfer = match Transfer::new(lba, count, write, &dev.geometry, MSC_MAX_BLOCKS) { - Ok(transfer) => transfer, - Err(past) => { - log!("usb-storage: {lba}+{count} is past the {} blocks this disk has", past.blocks); - return Err(BlockError::Device); - } - }; - - let dma = self.dma(); - let data = dma.subview(dev.block + MSC_DATA, MSC_DATA_LEN); - while let Some(batch) = transfer.next() { - let (bytes, offset) = (batch.bytes, batch.offset); - if let Host::From(src) = &host { - dma.copy_from(dev.block + MSC_DATA, &src[offset..offset + bytes]); - } - let reply = self.scsi(dev, &batch.cdb, Some(data.subview(0, bytes)), until); - let moved = transfer.answered(&batch, reply); - if moved.reported() { - dev.wrote(write); - } - match moved { - Moved::Whole => {} - Moved::Short { delivered } => { - log!("usb-storage: {delivered} of {bytes} B at block {}", batch.block); - return Err(BlockError::Device); - } - Moved::Refused(sense) => { - log_refusal(&batch.cdb, sense); - return Err(BlockError::Device); - } - Moved::Ended(fail) => return Err(block_error(fail)), - } - if let Host::Into(dst) = &mut host { - dma.copy_to(dev.block + MSC_DATA, &mut dst[offset..offset + bytes]); - } - } - Ok(()) - } - - /// One SCSI command with transport recovery applied and re-issued; - /// every CDB here is idempotent, so a re-issue is genuine. `until` is - /// checked only here, before a command starts and never before the one a - /// rung that took sends again — it costs the device nothing (no - /// TRB on a ring, no phase half done); checking inside [`Self::bot`] - /// would abandon a transfer the device is still going to answer. - /// - /// **A break is counted against the device and not against this call**, - /// so the run of breaks [`MAX_TRANSPORT_BREAKS`] bounds is the run the - /// device has produced, across however many operations the caller spent - /// on it; a completed round trip is what ends the run, and says so — the - /// call the break happened in may have ended on its budget before it - /// could ask again. - /// - /// **Everything the call does once its transport has broken is bounded, - /// rung by rung** ([`AFTER_BREAK`]): opened by the first break and closed - /// by whoever the call is — [`served`] for a block operation, the bind for - /// each of its commands — so a later command of the same operation spends - /// what the break left it, and no call inherits another's. - fn scsi(&mut self, dev: &mut MscDevice, cdb: &Cdb, data: DataPhase, until: Deadline) -> Reply { - let opcode = cdb.opcode(); - let data_out = data.is_some() && !cdb.data_in(); - // Named per line so a multi-disk boot's retry log attributes to the - // right disk. - let slot = self.slot(dev.slot_id); - loop { - // Not `Broken` either way: nothing was issued and `dev.failed` stays - // untouched. The command sent again after a rung that took is the - // call's to decide and not the operation's, whose budget the break - // may have spent (`toyos_xhci::call`). - match self.after_break.issue(crate::clock::nanos_since_boot(), until.nanos()) { - Ok(()) => {} - Err(NotIssued::Operation) => { - log!("usb-storage: {slot} SCSI {opcode:#04x} not issued: {}", - crate::block::OPERATION); - return Reply::Budget; - } - // A command re-issued with nothing left would have every wait - // cut at once, and count against the device a break that was - // the budget's. - Err(NotIssued::Call(why)) => { - log!("usb-storage: {slot} SCSI {opcode:#04x} not issued again: {why}"); - return Reply::Budget; - } - } - match self.bot(dev, cdb, data) { - Ok(Bot::Done { delivered }) => { - self.transport_came_back(dev, opcode); - return Reply::Ok { delivered }; - } - Ok(Bot::Failed) => { - self.transport_came_back(dev, opcode); - return Reply::Refused(self.request_sense(dev)); - } - // Not a transport that broke: a device that is no longer on - // the bus. Its port's own teardown gives the slot and the pool - // block back, and a recovery or a reset aimed at an empty port - // would only spend their bounds. - Err(gone @ Broke::Gone { .. }) => { - let broke = Told(&gone); - log!("usb-storage: {slot} transport broke on SCSI {opcode:#04x}: {broke}; \ - its port's teardown takes it from here"); - dev.failed = true; - dev.left = true; - // A hold for its device is part of this call, from the - // wait that saw it go. - self.after_break.open(self.bulk_began, AFTER_BREAK); - return Reply::Broken; - } - Err(why) => { - self.after_break.open(self.bulk_began, AFTER_BREAK); - let broke = Told(&why); - log!("usb-storage: {slot} transport broke on SCSI {opcode:#04x}: {broke}; \ - break {} of {MAX_TRANSPORT_BREAKS} running", dev.run.breaks().saturating_add(1)); - if !self.climb_until_in_step(dev, why.event(), why.left(data_out)) { - return Reply::Broken; - } - } - } - } - } - - /// What one break costs: it is counted, and the device climbs the next rung - /// of `toyos_xhci::ladder` — above every rung this run of breaks has - /// climbed, and never below where the break itself enters. A rung that did - /// not verify is the next break, so the climb goes on inside this call - /// until the device has answered under its own tag — `true`, the only - /// device the caller's command goes to again — or is offline, `false`. - /// - /// **Each rung is entered on its own bound** (`toyos_xhci::call`), so no - /// rung is starved by what the ones before it spent. - /// - /// `broke` is the transfer event that ended the round trip, where one did, - /// and `left` where it left the device. - fn climb_until_in_step( - &mut self, - dev: &mut MscDevice, - mut broke: Option<(Pipe, u32)>, - left: Left, - ) -> bool { - let slot = self.slot(dev.slot_id); - let mut climb = dev.run.broke(left); - loop { - let ladder::Climb { rung, skips_class_reset } = climb; - if skips_class_reset { - log!("usb-storage: {slot} is owed the data of the command that broke, so nothing \ - can be asked of it on the Bulk-Out: its port is reset with no class reset \ - before it"); - } - self.after_break.enter(rung, crate::clock::nanos_since_boot()); - let climbed = match rung { - Rung::ClassReset => self.reset_recovery(dev, broke), - Rung::PortReset => self.port_reset_recovery(dev, broke), - Rung::Offline => { - log!("usb-storage: {slot} broke {} times running; its port reset did not \ - bring the transport back", dev.run.breaks()); - self.take_offline(dev, broke); - return false; - } - }; - let Err(unverified) = climbed else { - self.after_break.took(rung); - return true; - }; - // Asked of the port once the rung has ended, since a USB2 port - // detects no disconnect while it drives a reset (xHCI 1.2 - // §4.19.1.1.2, note 57) and reads Enabled once the reset ends - // (§4.19.1.1.4). - let port = self.read_portsc(dev.port_idx); - let ended = Ended(rung, &unverified); - match dev.run.unverified(&unverified, port.holds()) { - AfterRung::Left => { - log!("usb-storage: {slot} {ended}, and port {} no longer holds the device \ - (PORTSC {:#010x}): its port's teardown takes it from here", - u32::from(dev.port_idx) + 1, port.raw()); - dev.failed = true; - dev.left = true; - return false; - } - AfterRung::Climbs { climb: next, broke: event } => { - log!("usb-storage: {slot} {ended}; break {} of {MAX_TRANSPORT_BREAKS} running", - dev.run.breaks()); - (climb, broke) = (next, event); - } - } - } - } - - /// A round trip completed: the run of breaks is over, and the log says so - /// where there was one. - fn transport_came_back(&mut self, dev: &mut MscDevice, opcode: u8) { - let breaks = dev.run.over(); - if breaks > 0 { - log!("usb-storage: {} SCSI {opcode:#04x} completed after {breaks} break(s) running; \ - the transport came back and the count is cleared", self.slot(dev.slot_id)); - } - } - - /// The ladder's last rung: the device did not answer after a port reset, or - /// kept breaking after one that it did. Every operation on the disk is - /// refused from here. - /// - /// **The last thing the device is sent is a reset**, whichever step it - /// stopped answering at: a device left holding a command it will never be - /// asked to finish is one the next host may not be able to enumerate. So - /// the port is reset whatever the endpoints could be taken to, and then the - /// controller is told (Reset Device, xHCI 1.2 §4.6.11), which leaves the - /// slot in Default with every endpoint but the control endpoint Disabled — - /// what the device now is, and a slot Disable Slot is defined over - /// (§4.6.4's note). Nothing is sent after it. - /// - /// **On its own bound**, entered by the climb, so the quiesce and the - /// reset's settle run whatever the rungs before spent. - /// - /// **The slot is given back by whoever the port says holds it.** A bound - /// disk's goes back from the poll ([`MscDevice::take_slot_owed`]). A disk - /// still inside its bind holds no slot as far as its port knows: [`bind`] - /// answers with where the slot goes, and the enumeration that failed either - /// gives it back, once, or hands it to the port. - fn take_offline(&mut self, dev: &mut MscDevice, broke: Option<(Pipe, u32)>) { - dev.failed = true; - let slot = self.slot(dev.slot_id); - let in_state = self.endpoint_state(dev.dev_block, dev.in_dci()); - let out_state = self.endpoint_state(dev.dev_block, dev.out_dci()); - let stopped = reset_recovery::nothing_to_stop(in_state, out_state) - || self.quiesce_bulk_pair(dev, broke, "stopping it before its port is reset"); - let reset = self.reset_port(dev, "taking it offline"); - dev.left |= reset == AfterReset::Left; - // A Context State Error is a slot already in Default (§4.6.11): the - // port rung got as far as its own Reset Device and no further. - let told = reset.finished() - && matches!( - self.command_code(reset_device_trb(dev.slot_id), "Reset Device"), - Some(CC_SUCCESS | CC_CONTEXT_STATE_ERROR) - ); - let goes = reset_recovery::slot_after_offline(stopped || told); - dev.slot_goes = Some(goes); - log!( - "usb-storage: {slot} is offline: both bulk endpoints Stopped={stopped}, port {} \ - reset={} and nothing sent after it, Reset Device={told}, {}; every operation on it \ - is refused from here", - u32::from(dev.port_idx) + 1, - reset.finished(), - match goes { - SlotGoes::Back => "its slot goes back to the controller", - SlotGoes::WithTheUnplug => "its slot is kept until the unplug", - } - ); - } - - /// The most reset this device's port has, waited for on what the rung it is - /// part of may still spend, its change flags consumed as an enumeration's - /// are so the port machine reads none of them as a replug. Says what the - /// port read before and after, and answers with what the reset left. - /// - /// The write is made whatever is left of the bound: it costs nothing, and - /// it is what leaves the device in its Default state. - fn reset_port(&mut self, dev: &mut MscDevice, why: &str) -> AfterReset { - let port_idx = dev.port_idx; - let before = self.read_portsc(port_idx); - let protocol = self.protocols.of(port_idx); - let kind = port::offline_reset(protocol); - let here = before.holds(); - let finished = here && { - self.write_portsc(port_idx, port::reset_write(kind, before)); - dev.reset_at = Some(crate::clock::nanos_since_boot()); - self.settles_within_call(|| self.read_portsc(port_idx).reset_finished()) - }; - let after = self.read_portsc(port_idx); - if finished { - self.write_portsc(port_idx, port::enumeration_ack(Some(kind), after)); - } - let left = ladder::after_reset( - finished, - here && after.connected(), - after.enabled(), - after.speed(), - dev.enumerated.speed, - ); - log!( - "xHCI: {} port {} reset while {why} ({} on {}): PORTSC {:#010x} then {:#010x}, link \ - {:?}, speed {}: {left}", - self.slot(dev.slot_id), - u32::from(port_idx) + 1, - match kind { - port::Reset::Hot => "hot", - port::Reset::Warm => "warm", - }, - match protocol { - Some(toyos_xhci::Protocol::Usb2) => "a USB2 port", - Some(toyos_xhci::Protocol::Usb3) => "a USB3 port", - None => "a port of no named protocol", - }, - before.raw(), - after.raw(), - after.link_state(), - after.speed(), - ); - left - } - - /// The ladder's second rung: the port reset, and the enumeration a reset - /// owes (xHCI 1.2 §4.19.5), on the slot, the pool block and the disk number - /// the device already has — so a mount on it carries on. The steps and - /// their order are `ladder::PORT_RESET`'s; this takes them, one blocking - /// command or control transfer at a time, and ends on the device's answer - /// to TEST UNIT READY. - fn port_reset_recovery(&mut self, dev: &mut MscDevice, broke: Option<(Pipe, u32)>) -> Result<(), Unverified> { - let slot = self.slot(dev.slot_id); - for step in ladder::PORT_RESET { - let took = match step { - PortStep::Quiesce => { - self.quiesce_bulk_pair(dev, broke, "stopping it before its port is reset") - } - // `reset_port` has said which way it did not. - PortStep::Reset => self.reset_port(dev, RECOVERING).goes_on(), - PortStep::Settle => { - let _ = crate::clock::settles( - self.after_break - .wait_left(crate::clock::nanos_since_boot(), ladder::RESET_RECOVERY_NS), - || false, - ); - true - } - PortStep::ResetDevice => { - self.run_command(reset_device_trb(dev.slot_id), "Reset Device") - } - PortStep::Address => { - let trb = address_again_trb(self, dev); - self.run_command(trb, "Address Device (after the port reset)") - } - PortStep::Configure => { - let (slot_id, block) = (dev.slot_id, dev.dev_block); - let value = u16::from(dev.enumerated.configuration); - let set = self - .control_transfer(slot_id, block, &mut dev.ep0_ring, 0x00, 0x09, value, 0, None, 0); - if !set.done() { - log!("usb-storage: {slot} would not take SET_CONFIGURATION({value}) after \ - its port reset: {set}"); - } - set.done() - } - PortStep::AddEndpoints => self.configure_bulk_pair( - dev, - configure::Configure::First { - speed: dev.enumerated.speed, - port: dev.port_idx + 1, - }, - ), - }; - if !took && ladder::ends_the_rung(step) { - return Err(Unverified::Failed); - } - } - match self.bot(dev, &Cdb::TEST_UNIT_READY, None) { - Ok(answer) => { - log!("usb-storage: {slot} the port reset took: addressed and configured again, the \ - device answered TEST UNIT READY under its own tag {:#x}", dev.tag); - self.take_held_sense(dev, answer); - Ok(()) - } - Err(why) => Err(Unverified::OutOfStep(why)), - } - } - - /// Status 1 on a rung's TEST UNIT READY is sense the device holds for - /// whoever asks next, which would otherwise be the command the rung is for. - fn take_held_sense(&mut self, dev: &mut MscDevice, answer: Bot) { - if matches!(answer, Bot::Failed) { - let sense = self.request_sense(dev); - log!("usb-storage: {} held sense {sense} after its recovery", self.slot(dev.slot_id)); - } - } - - /// REQUEST SENSE through `bot` directly, so it cannot recurse into asking - /// for sense about itself. - fn request_sense(&mut self, dev: &mut MscDevice) -> Sense { - let dma = self.dma(); - let scratch = dma.subview(dev.block + MSC_SCRATCH, MSC_SCRATCH_LEN); - scratch.zero(); - let region = scratch.subview(0, scsi::SENSE_BYTES); - match self.bot(dev, &Cdb::REQUEST_SENSE, Some(region)) { - Ok(Bot::Done { delivered }) => { - let mut response = [0u8; scsi::SENSE_BYTES]; - dma.copy_to(dev.block + MSC_SCRATCH, &mut response); - Sense::of(&response, delivered).unwrap_or_else(|why| { - log!("usb-storage: {} answered sense this driver does not read: {why:?}", self.slot(dev.slot_id)); - Sense::NONE - }) - } - _ => Sense::NONE, - } - } - - /// One Bulk-Only round trip, as `toyos_xhci::bot::RoundTrip` asks for it: - /// each transfer queued and waited for in place. - fn bot(&mut self, dev: &mut MscDevice, cdb: &Cdb, data: DataPhase) -> Result { - crate::block::census::command_issued(); - let data_in = cdb.data_in(); - // The length the device is told to move is the region's own, so the - // only bound left to state is this driver's largest transfer. - let (data_phys, data_len) = match data { - Some(region) => { - assert!( - region.size() <= MSC_DATA_LEN, - "usb-storage: a {} B data phase, past the {MSC_DATA_LEN} B this driver rings", - region.size(), - ); - (region.device_addr(), region.size() as u32) - } - None => (0, 0), - }; - - let dma = self.dma(); - let tag = dev.next_tag(); - #[cfg(feature = "stack-witness")] - let entered_with = block_witness(dev); - dma.copy_from(dev.block + MSC_CBW, &bot::cbw(tag, data_len, cdb)); - - // **From here the device is one this kernel has spoken a command to**, - // and stays one until the CSW below is in hand: a reset between any two - // phases leaves it waiting, which is what `stop::settle_commands` - // finishes. Opened before the CBW's transfer is queued, so nothing - // reaches the controller unpublished. - let open = stop::OpenCommand::begin(stop::Device { - block: dma.subview(dev.block, MSC_STRIDE), - slot: dev.slot_id, - in_dci: dev.in_dci(), - out_dci: dev.out_dci(), - ctx: dma.subview(dev.dev_block + super::super::DEV_OUT_CTX, 32 * self.context_size), - ctx_size: self.context_size as u32, - data_in, - }); - #[cfg(feature = "boot-actuators")] - let write = cdb.opcode() == scsi::WRITE_10; - - let (mut trip, mut act) = RoundTrip::begin(tag, data_len, cdb); - loop { - let answer = match act { - bot::Act::Command => { - let cbw_phys = dma.device_addr() + (dev.block + MSC_CBW) as u64; - completed(self.bulk(dev, false, cbw_phys, CBW_LEN as u32, Phase::Command, &open)) - } - bot::Act::Data(pipe) => { - open.at(Phase::DataOwed, &dev.in_ring, &dev.out_ring); - #[cfg(feature = "boot-actuators")] - if write { - transport_break::arm(); - } - completed(self.bulk(dev, pipe == Pipe::In, data_phys, data_len, Phase::Data, &open)) - } - bot::Act::Status => { - open.at(Phase::StatusOwed, &dev.in_ring, &dev.out_ring); - super::super::zero_dma(dma, dev.block + MSC_CSW, CSW_LEN); - let csw_phys = dma.device_addr() + (dev.block + MSC_CSW) as u64; - completed(self.bulk(dev, true, csw_phys, CSW_LEN as u32, Phase::Status, &open)) - } - bot::Act::Restart { pipe, then } => { - let took = self.restart_bulk(dev, pipe == Pipe::In); - // The recovery rebuilt the ring, so the point the account - // reads is republished before anything else reaches the - // controller. - if took { - open.at(then, &dev.in_ring, &dev.out_ring); - } - bot::Answer::Restarted(took) - } - }; - match trip.answered(answer) { - bot::Next::Act(next, then) => (trip, act) = (next, then), - bot::Next::Csw(due) => { - #[cfg(feature = "stack-witness")] - block_witness_holds(dev, entered_with); - let mut csw = [0u8; CSW_LEN]; - dma.copy_to(dev.block + MSC_CSW, &mut csw); - return due.judge(&csw); - } - bot::Next::Broke(broke) => return Err(broke), - } - } - } - - /// One Normal TRB on a bulk endpoint, and its completion. - /// - /// `phase` is the leg of the round trip this TRB is, published between the - /// enqueue and the doorbell. - fn bulk( - &mut self, - dev: &mut MscDevice, - in_dir: bool, - phys: u64, - len: u32, - phase: Phase, - open: &stop::OpenCommand, - ) -> Result<(u32, u32), Quiet> { - let (dci, ring) = if in_dir { - (dev.in_dci(), &mut dev.in_ring) - } else { - (dev.out_dci(), &mut dev.out_ring) - }; - let mut trb = Trb::ZERO; - trb.param = phys; - trb.status = len; - // ISP so a device that sends less than asked reports it instead of - // leaving the transfer outstanding, IOC so it reports at all. - trb.control = TRB_NORMAL | (1 << 5) | (1 << 2); - let at = ring.enqueue(trb); - let slot = dev.slot_id; - // Before the doorbell, so no transfer is visible to the controller - // without a reset being able to see the ring it went on. - open.at(phase, &dev.in_ring, &dev.out_ring); - self.bulk_began = crate::clock::nanos_since_boot(); - self.ring_doorbell(slot, dci); - #[cfg(feature = "boot-actuators")] - if transport_break::take() { - return Err(Quiet::Staged); - } - self.wait_transfer(slot, dci, at) - } - - /// One of this disk's bulk endpoints, packaged for recovery; which - /// command is legal is [`XhciController::restart_endpoint`]'s to decide. - fn bulk_endpoint<'a>(dev: &'a mut MscDevice, in_dir: bool) -> Restart<'a> { - let (dci, ep_addr, ring_off) = if in_dir { - (dev.in_dci(), dev.pair.in_ep.addr, MSC_IN_RING) - } else { - (dev.out_dci(), dev.pair.out_ep.addr, MSC_OUT_RING) - }; - Restart { - slot_id: dev.slot_id, - ctx_block: dev.dev_block, - dci, - ep_addr, - ring_at: dev.block + ring_off, - ring: if in_dir { &mut dev.in_ring } else { &mut dev.out_ring }, - ep0_ring: &mut dev.ep0_ring, - } - } - - /// One of this disk's bulk endpoints, back to a state that runs TRBs. - fn restart_bulk(&mut self, dev: &mut MscDevice, in_dir: bool) -> bool { - self.restart_endpoint(Self::bulk_endpoint(dev, in_dir)) - } - - /// Bulk-Only Transport §5.3.4's Reset Recovery, with the commands an xHC - /// owes ahead of it: `toyos_xhci::reset_recovery` decides the steps and - /// this takes them, one blocking command or control transfer at a time. - /// - /// A command that did not take ends it, since the requests after it assume - /// both endpoints are off their transfers; a request that did not is - /// followed by the rest, so the device is left with both pipes cleared - /// whatever the next rung then does with it. Either is [`ladder::Unverified::Failed`]. - /// - /// **Then the device is asked, and only its answer says the recovery - /// took**: TEST UNIT READY, whose status must carry that command's own tag. - /// - /// `broke` is the transfer event that ended the round trip, where one did. - fn reset_recovery(&mut self, dev: &mut MscDevice, broke: Option<(Pipe, u32)>) -> Result<(), Unverified> { - if !self.quiesce_bulk_pair(dev, broke, "recovering") { - return Err(Unverified::Failed); - } - let slot = self.slot(dev.slot_id); - let mut recovered = true; - for step in reset_recovery::AFTER_QUIESCE { - let took = match step { - Step::Reconfigure => self.configure_bulk_pair(dev, configure::Configure::Again), - Step::MassStorageReset => { - let (slot_id, block, iface) = (dev.slot_id, dev.dev_block, dev.pair.iface_num); - let reset = self.control_transfer( - slot_id, block, &mut dev.ep0_ring, 0x21, 0xFF, 0, u16::from(iface), None, 0, - ); - if !reset.done() { - log!("usb-storage: {slot} would not take a Bulk-Only Reset: {reset}"); - } - reset.done() - } - Step::ClearHalt(pipe) => { - let ep_addr = dev.ep_addr(pipe); - self.clear_endpoint_halt(dev.slot_id, dev.dev_block, &mut dev.ep0_ring, ep_addr) - } - }; - if !took { - recovered = false; - if step == Step::Reconfigure { - break; - } - } - } - if !recovered { - return Err(Unverified::Failed); - } - match self.bot(dev, &Cdb::TEST_UNIT_READY, None) { - Ok(answer) => { - log!("usb-storage: {slot} Reset Recovery took: the device answered TEST UNIT \ - READY under its own tag {:#x}", dev.tag); - self.take_held_sense(dev, answer); - Ok(()) - } - Err(why) => Err(Unverified::OutOfStep(why)), - } - } - - /// Both bulk endpoints to Stopped. Every decision is - /// `reset_recovery::Quiescing`'s — which command, what an answer means, and - /// when to stop asking — and this issues what it is told and reports what - /// came back; the loop ends where the plan does. `broke` is the transfer - /// event that ended the round trip, and `why` ends the line that names each - /// endpoint's field. - fn quiesce_bulk_pair( - &mut self, - dev: &mut MscDevice, - broke: Option<(Pipe, u32)>, - why: &str, - ) -> bool { - let slot = self.slot(dev.slot_id); - let mut plan = Quiescing::begin(broke); - let mut said = false; - // What each Stop Endpoint's own transfer event said of the TRB it - // stopped inside, Bulk-In first. - let mut cut = [None; 2]; - loop { - let in_state = self.endpoint_state(dev.dev_block, dev.in_dci()); - let out_state = self.endpoint_state(dev.dev_block, dev.out_dci()); - if !core::mem::replace(&mut said, true) { - log!("xHCI: {slot} endpoint {} is {in_state}, {why}", dev.in_dci()); - log!("xHCI: {slot} endpoint {} is {out_state}, {why}", dev.out_dci()); - } - let (cmd, pipe) = match plan.look(in_state, out_state) { - Look::Stopped => { - self.log_unreached(dev, cut); - return true; - } - Look::Command(cmd, pipe) => (cmd, pipe), - Look::GaveUp(gave_up) => { - match gave_up { - GaveUp::NeedsConfigure(pipe, state) => { - log_unrecoverable(slot, dev.dci(pipe), state) - } - GaveUp::Refused(cmd, code) => { - log!("xHCI: {} failed: {}", cmd.name(), Completion(code)) - } - // `command_code` has said so already. - GaveUp::Silent(_) => {} - GaveUp::Contradicted(pipe) => log!( - "xHCI: {slot} endpoint {} answered {} to every command its states \ - define", - dev.dci(pipe), - Completion(CC_CONTEXT_STATE_ERROR) - ), - GaveUp::OutOfLooks => log!( - "xHCI: {slot} bulk pair is not Stopped after {} looks", - reset_recovery::MOST_LOOKS - ), - } - return false; - } - }; - let (slot_id, dci) = (dev.slot_id, dev.dci(pipe)); - let (ring, ring_at) = dev.ring_mut(pipe); - let trb = self.recovery_trb(cmd, slot_id, dci, ring, ring_at); - self.stopped = None; - let code = self.command_code(trb, cmd.name()); - if let Some((_, _, code, left)) = - self.stopped.take().filter(|(s, d, ..)| (*s, *d) == (slot_id, dci)) - { - cut[usize::from(pipe == Pipe::Out)] = Some((code, left)); - } - if let Answered::Moved { from } = plan.answered(code) { - log!( - "xHCI: {slot} endpoint {dci} was not {from}: {} answered {}; looking again", - cmd.name(), - Completion(CC_CONTEXT_STATE_ERROR) - ); - } - } - } - - /// What the controller had not reached on each Stopped pipe's ring, off the - /// TR Dequeue Pointer it saves into the output context when the endpoint - /// leaves Running (xHCI 1.2 §4.6.9), and what each Stop Endpoint's own - /// transfer event said of the TRB it stopped inside: `cut` is its - /// completion code and TRB Transfer Length — the bytes of that TRB the - /// controller had not moved — Bulk-In first. Read and said, never acted on: - /// how much of a transfer this driver stopped waiting for had reached the - /// device is what the device is holding when the class reset reaches it. - fn log_unreached(&self, dev: &MscDevice, cut: [Option<(u32, u32)>; 2]) { - struct Left { - dci: u8, - unreached: Result, - cut: Option<(u32, u32)>, - } - impl core::fmt::Display for Left { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "endpoint {} ", self.dci)?; - match &self.unreached { - Ok(unreached) => write!(f, "with {unreached} TRB(s) unreached")?, - Err(why) => write!(f, "with {why}")?, - } - match self.cut { - Some((code, left)) => { - write!(f, " and a stop event of {}, {left} B unmoved", Completion(code)) - } - None => f.write_str(" and no stop event"), - } - } - } - let dma = self.dma(); - let left = |pipe: Pipe, ring: &TrbRing, cut| { - let ctx = dev.dev_block - + super::super::DEV_OUT_CTX - + usize::from(dev.dci(pipe)) * self.context_size; - // Volatile through `Dma`: the controller writes both by DMA. - let dequeue = u64::from(dma.read::(ctx + 8)) - | (u64::from(dma.read::(ctx + 12)) << 32); - let queued = toyos_xhci::Ring { - base: ring.base_phys, - trbs: super::super::RING_SIZE as u16, - tail: ring.tail, - }; - Left { dci: dev.dci(pipe), unreached: queued.pending(dequeue), cut } - }; - log!( - "xHCI: {} bulk pair Stopped: {}; {}", - self.slot(dev.slot_id), - left(Pipe::In, &dev.in_ring, cut[0]), - left(Pipe::Out, &dev.out_ring, cut[1]) - ); - } - - /// Configure Endpoint making the bulk pair on fresh rings (xHCI 1.2 §4.6.6): - /// dropped and added by a class reset, where they come back Running at - /// their rings' first TRB with their data toggle or sequence number zeroed - /// as the ClearFeature(ENDPOINT_HALT) the device is about to get zeroes its - /// own; added by a port reset, whose Reset Device left them Disabled. The - /// input context is the bind's own writer's, so the pair is described - /// exactly as it was configured. - /// - /// **The controller's own word is read back and printed, not judged.** No - /// doorbell has been rung on either ring, so an endpoint the output context - /// calls Running is one the command added. The field lags the endpoint - /// (§4.8.3), so a disk is not taken offline on it: the next transfer is - /// what says whether the pair runs. - fn configure_bulk_pair(&mut self, dev: &mut MscDevice, how: configure::Configure) -> bool { - let dma = self.dma(); - dev.in_ring = TrbRing::init(dma.subview(dev.block + MSC_IN_RING, PAGE)); - dev.out_ring = TrbRing::init(dma.subview(dev.block + MSC_OUT_RING, PAGE)); - let input_ctx = bulk_pair_input_context( - self, - dev.block + MSC_INPUT_CTX, - &dev.pair, - &dev.in_ring, - &dev.out_ring, - how, - ); - let (what, did) = match how { - configure::Configure::Again => { - ("Configure Endpoint (the bulk pair dropped and added)", "dropped and added") - } - configure::Configure::First { .. } => { - ("Configure Endpoint (the bulk pair added after the port reset)", "added again") - } - }; - let mut configure = Trb::ZERO; - configure.param = input_ctx.device_addr(); - configure.control = TRB_CONFIGURE_EP | (u32::from(dev.slot_id) << 24); - if !self.run_command(configure, what) { - return false; - } - let in_state = self.endpoint_state(dev.dev_block, dev.in_dci()); - let out_state = self.endpoint_state(dev.dev_block, dev.out_dci()); - let slot = self.slot(dev.slot_id); - log!("xHCI: {slot} bulk pair {did}: endpoint {} is {in_state}, endpoint {} is {out_state}", - dev.in_dci(), dev.out_dci()); - true - } -} - -/// Reset Device for `slot_id` (xHCI 1.2 §6.4.3.10): the slot and nothing else. -fn reset_device_trb(slot_id: u8) -> Trb { - let mut trb = Trb::ZERO; - trb.control = TRB_RESET_DEVICE | (u32::from(slot_id) << 24); - trb -} - -/// Address Device for a device the port rung has just reset, from the Default -/// state Reset Device left its slot in (§4.6.5, BSR clear): the slot context -/// the enumeration gave it, and the control endpoint resuming where its ring -/// stands. In the disk's own page, as [`bulk_pair_input_context`] is. -fn address_again_trb(ctrl: &XhciController, dev: &MscDevice) -> Trb { - let input_ctx = super::super::zero_dma(ctrl.dma(), dev.block + MSC_INPUT_CTX, PAGE); - ctrl.write_ctx32(input_ctx, 0, 1, 0x3); // A0 and A1: the slot and the control endpoint - ctrl.write_ctx32(input_ctx, 1, 0, (u32::from(dev.enumerated.speed) << 20) | (1 << 27)); - ctrl.write_ctx32(input_ctx, 1, 1, (u32::from(dev.port_idx) + 1) << 16); - // CErr 3, EP Type Control, and the packet size the descriptor gave. - let ep0 = (3u32 << 1) | (4u32 << 3) | (u32::from(dev.enumerated.ep0_packet) << 16); - ctrl.write_ctx32(input_ctx, usize::from(EP0_DCI) + 1, 1, ep0); - let dequeue = dev.ep0_ring.dequeue(); - ctrl.write_ctx32(input_ctx, usize::from(EP0_DCI) + 1, 2, dequeue as u32); - ctrl.write_ctx32(input_ctx, usize::from(EP0_DCI) + 1, 3, (dequeue >> 32) as u32); - ctrl.write_ctx32(input_ctx, usize::from(EP0_DCI) + 1, 4, 8); - let mut trb = Trb::ZERO; - trb.param = input_ctx.device_addr(); - trb.control = TRB_ADDRESS_DEVICE | (u32::from(dev.slot_id) << 24); - trb -} - -/// The input context that configures a device's bulk pair, written into the -/// zeroed page at `at`. Every dword is `toyos_xhci::configure`'s: nothing about -/// what the controller is told is decided here. -/// -/// **The bind's is the controller's shared page and a rung's is the disk's -/// own.** A rung runs inside a disk call, beside whatever enumeration another -/// port has outstanding, and that enumeration's command may still be reading -/// the shared page. -fn bulk_pair_input_context( - ctrl: &XhciController, - at: usize, - info: &MscInterface, - in_ring: &TrbRing, - out_ring: &TrbRing, - configure: configure::Configure, -) -> Dma<'static> { - let input_ctx = super::super::zero_dma(ctrl.dma(), at, PAGE); - let endpoint = |ep: &Endpoint, ring: &TrbRing| BulkEndpoint { - dci: ep.dci(), - max_packet: ep.max_packet, - max_burst: ep.max_burst, - dequeue: ring.dequeue(), - }; - let words = - configure::bulk_pair(endpoint(&info.in_ep, in_ring), endpoint(&info.out_ep, out_ring), configure); - for word in words { - ctrl.write_ctx32(input_ctx, word.context, word.dword, word.value); - } - input_ctx -} - -/// One mass-storage device's pool block and the two bulk rings its endpoint -/// contexts name, as [`prepare`] left them — not rebuilt here, since -/// `TrbRing::init` would zero memory the controller now reads. -#[derive(Clone, Copy)] -pub(in crate::drivers::xhci) struct MscRings { - /// Which of [`super::super::MSC_BLOCKS`] this is; teardown gives it back. - at: usize, - block: usize, - in_ring: TrbRing, - out_ring: TrbRing, - /// The port the device is on, which the ladder resets. - port_idx: u8, -} - -/// Claim a pool block and write its two bulk endpoints into the input -/// context, ready for the Configure Endpoint the sequence issues. -/// Claimed before the endpoints are configured, released only by teardown, -/// so the block is never reissued while the previous holder's contexts -/// still name it. `None` when the pool is out — a refusal, not a failure. -pub(in crate::drivers::xhci) fn prepare( - ctrl: &mut XhciController, - slot_id: u8, - speed: u8, - port_idx: u8, - info: &MscInterface, -) -> Option { - let Some((at, block)) = ctrl.claim_msc_block(port_idx) else { - log!("usb-storage: slot {slot_id} is the {}th disk; this driver serves {}", - ctrl.msc_blocks_taken() + 1, super::super::MSC_BLOCKS); - return None; - }; - - let dma = ctrl.dma(); - let in_ring = TrbRing::init(dma.subview(block + MSC_IN_RING, PAGE)); - let out_ring = TrbRing::init(dma.subview(block + MSC_OUT_RING, PAGE)); - let first = configure::Configure::First { speed, port: port_idx + 1 }; - bulk_pair_input_context(ctrl, OFF_INPUT_CTX, info, &in_ring, &out_ring, first); - Some(MscRings { at, block, in_ring, out_ring, port_idx }) -} - -/// Ask the disk what it is and register it if it's one this driver serves. -/// `dev_block` is the device's own block, where its EP0 ring already lives. -/// The last blocking path a scheduler pass can reach: bring-up has to run -/// somewhere and there is no other context that may block. -/// Every failure path already logs; what the answer carries is who keeps the -/// slot, which the caller must act on. -/// -/// `described` is the device descriptor's identity and its iSerialNumber. -#[allow(clippy::too_many_arguments)] -pub(in crate::drivers::xhci) fn bind( - ctrl: &mut XhciController, - ep0_ring: TrbRing, - slot_id: u8, - dev_block: usize, - rings: MscRings, - info: &MscInterface, - enumerated: Enumerated, - described: (UsbId, u8), -) -> Bind { - let MscRings { at, block, in_ring, out_ring, port_idx } = rings; - let (usb, serial_index) = described; - let mut dev = MscDevice { - slot_id, - pair: *info, - port_idx, - enumerated, - block, - dev_block, - ep0_ring, - in_ring, - out_ring, - tag: 0, - geometry: Geometry::NONE, - run: Run::NONE, - failed: false, - slot_goes: None, - no_write_cache: false, - debt: Debt::NONE, - identity: Identity { - usb, - serial: Serial::Absent, - inquiry: [0; 28], - sectors: 0, - sector_bytes: 0, - }, - reset_at: None, - left: false, - }; - - match bring_up(ctrl, &mut dev) { - Up::Ready => {} - // A bind that failed without going offline spoke to a transport that - // answered: its pair has nothing on its rings. - Up::Refused => return Bind::Refused(dev.slot_goes.unwrap_or(SlotGoes::Back)), - Up::NotReady => return Bind::NotReady, - } - dev.identity.serial = read_serial(ctrl, &mut dev, serial_index); - log!("usb-storage: slot {slot_id} serial number {}", dev.identity.serial); - // Machine-wide index: what `usb_storage::handle` looks up by and a mount - // holds for life, so it must not move when another controller binds or - // loses a disk — and a device that is a disk this driver's reset lost - // takes that disk's back. - if let Some((index, losses, owed)) = ctrl.adopt(&dev.identity, port_idx) { - dev.debt = Debt::adopted(losses); - log!("usb-storage: disk {index} came back on port {} slot {slot_id} as the same device \ - (USB {:04x}:{:04x}, serial number {}, {} blocks of {} B), msc_block +{:#x}; its \ - volume carries on{}", - u32::from(port_idx) + 1, usb.vendor, usb.product, dev.identity.serial, dev.geometry.blocks(), - dev.geometry.sector_bytes(), block, - if owed { OWED_A_FLUSH } else { "" }); - ctrl.msc[at].disk = Some(Disk { index, dev }); - return Bind::Bound; - } - let index = super::super::DISKS_BOUND.fetch_add(1, core::sync::atomic::Ordering::Relaxed); - log!( - "usb-storage: disk {index} ready on slot {slot_id}, {} blocks of {} B \ - ({} MiB), msc_block +{:#x}", - dev.geometry.blocks(), - dev.geometry.sector_bytes(), - dev.geometry.blocks() * u64::from(HOST_BLOCK) / (1024 * 1024), - block - ); - ctrl.msc[at].disk = Some(Disk { index, dev }); - Bind::Bound -} - -/// What the adopting line adds for a disk whose device came back owing a flush. -const OWED_A_FLUSH: &str = ", and it left owing a flush of writes it had reported complete, so \ - the flush of each writer whose writes they were fails"; - -/// What came of a bind. -pub(in crate::drivers::xhci) enum Bind { - /// A disk, which holds the slot. - Bound, - /// No disk; the caller releases the claimed block at the unplug, and the - /// slot goes where this says. - Refused(SlotGoes), - /// The device answered and never became ready inside [`READY_BUDGET`]; it - /// holds nothing on its rings, and its slot goes back. - NotReady, -} - -/// How far [`bring_up`] got. -enum Up { - /// A disk with a size. - Ready, - /// It answered, and never said it was ready: a device a later enumeration - /// may find ready. - NotReady, - /// Anything else, each said by name. - Refused, -} - -/// Everything between a configured interface and a disk with a size, as -/// `toyos_scsi::BringUp` asks for it. -fn bring_up(ctrl: &mut XhciController, dev: &mut MscDevice) -> Up { - let slot = dev.slot_id; - let dma = ctrl.dma(); - let scratch = dma.subview(dev.block + MSC_SCRATCH, MSC_SCRATCH_LEN); - // No caller budget here: bring-up isn't an operation with a - // `BlockDevice` handle to answer — it answers only to `READY_BUDGET` and - // `USB_TIMEOUT_NS`. - let until = Deadline::never(); - let mut pending = None; - let mut read = [0u8; MSC_SCRATCH_LEN]; - let (mut up, mut ask) = BringUp::begin(crate::clock::nanos_since_boot() + READY_BUDGET.nanos()); - loop { - let heard = match ask { - scsi::Ask::TestUnitReady => match ctrl.bot(dev, &Cdb::TEST_UNIT_READY, None) { - Ok(Bot::Done { .. }) => Heard::Good, - Ok(Bot::Failed) => Heard::CheckCondition, - Err(why) => { - let broke = Told(&why); - log!("usb-storage: slot {} broke on TEST UNIT READY: {broke}", dev.slot_id); - pending = Some(why); - Heard::Broke - } - }, - scsi::Ask::RequestSense => Heard::Sense(ctrl.request_sense(dev)), - scsi::Ask::Recover => { - let why = pending.take().expect("a recovery is asked for only after a break"); - ctrl.after_break.open(ctrl.bulk_began, AFTER_BREAK); - // A rung ends on this same command answered, so the run of - // breaks it counted is over when it says the device is in step. - if ctrl.climb_until_in_step(dev, why.event(), Left::Elsewhere) { - dev.run.over(); - } - ctrl.after_break = AfterBreak::CLOSED; - Heard::Recovered { offline: dev.failed } - } - scsi::Ask::Read(query) => { - let (cdb, len) = (query.cdb(), query.allocation()); - scratch.zero(); - // `subview` refuses a command asking for more than the scratch - // buffer holds. Each command of a bind is a call of its own. - let reply = ctrl.scsi(dev, &cdb, Some(scratch.subview(0, len)), until); - ctrl.after_break = AfterBreak::CLOSED; - match reply { - Reply::Ok { delivered } => { - dma.copy_to(dev.block + MSC_SCRATCH, &mut read[..len]); - Heard::Data { bytes: &read[..len], delivered } - } - Reply::Refused(sense) => { - log_refusal(&cdb, sense); - Heard::Unanswered - } - Reply::Broken | Reply::Budget => Heard::Unanswered, - } - } - }; - let end = match up.heard(heard, crate::clock::nanos_since_boot()) { - scsi::Next::Ask(next, then) => { - (up, ask) = (next, then); - continue; - } - scsi::Next::Disk(next, inquiry, then) => { - log!("usb-storage: slot {slot} vendor {} product {}", - Printable(inquiry.vendor()), Printable(inquiry.product())); - dev.identity.inquiry = inquiry.0; - (up, ask) = (next, then); - continue; - } - scsi::Next::Up(end) => end, - }; - return match end { - scsi::Up::Ready(geometry) => { - dev.geometry = geometry; - dev.identity.sectors = geometry.sectors(); - dev.identity.sector_bytes = geometry.sector_bytes(); - Up::Ready - } - scsi::Up::Unready { sense, offline } => { - log!("usb-storage: slot {slot} never became ready, sense {sense}"); - if offline { Up::Refused } else { Up::NotReady } - } - scsi::Up::Refused(why) => { - match why { - Refusal::Unanswered(query) => { - log!("usb-storage: slot {slot} would not answer {}", query.named()); - } - Refusal::NotADisk(peripheral) => { - log!("usb-storage: slot {slot} is SCSI peripheral type {peripheral:#04x}, \ - not a disk"); - } - Refusal::SectorSize(bytes) => { - log!("usb-storage: slot {slot} reports {bytes}-byte blocks; this driver \ - serves 4096-byte blocks and needs 512..=4096"); - } - Refusal::PastRead10 { last_lba } => { - log!("usb-storage: slot {slot} has {} sectors; this driver issues READ(10) \ - and addresses 2^32", u128::from(last_lba) + 1); - } - Refusal::LessThanABlock { sectors, sector_bytes } => { - log!("usb-storage: slot {slot} holds {sectors} sectors of {sector_bytes} B, \ - less than one 4096-byte block"); - } - } - Up::Refused - } - }; - } -} - -/// The serial number string a device's iSerialNumber names (USB 2.0 §9.6.1), -/// in the first language its string descriptor zero offers (§9.6.7): the one -/// field that tells two units of one model apart. -/// -/// Into the disk's own data buffer, which nothing else uses before the bind -/// hands the disk over. -fn read_serial(ctrl: &mut XhciController, dev: &mut MscDevice, index: u8) -> Serial { - if index == 0 { - return Serial::Absent; - } - let dma = ctrl.dma(); - let phys = dma.device_addr() + (dev.block + MSC_DATA) as u64; - let mut get = |ctrl: &mut XhciController, index: u8, language: u16| -> Option<([u8; 255], usize)> { - dma.subview(dev.block + MSC_DATA, 255).zero(); - let asked = ctrl.control_transfer( - dev.slot_id, dev.dev_block, &mut dev.ep0_ring, 0x80, 0x06, - 0x0300 | u16::from(index), language, Some(phys), 255, - ); - let Control::Done { delivered } = asked else { - log!("usb-storage: slot {} would not give string descriptor {index}: {asked}", - dev.slot_id); - return None; - }; - let delivered = usize::from(delivered.min(255)); - let mut arrived = [0u8; 255]; - dma.copy_to(dev.block + MSC_DATA, &mut arrived[..delivered]); - Some((arrived, delivered)) - }; - let Some((languages, delivered)) = get(ctrl, 0, 0) else { return Serial::Unread }; - let Some(language) = identity::first_language(&languages[..delivered]) else { return Serial::Unread }; - match get(ctrl, index, language) { - Some((arrived, delivered)) => Serial::from_descriptor(&arrived[..delivered]), - None => Serial::Unread, - } -} - -/// Read `count` 4 KiB blocks at `lba`. On `Err` the transfer did not happen -/// and `buf` holds nothing the caller may believe. -/// The caller must be inside a block-device operation -/// ([`crate::block::begin_operation`]); a call with no budget established -/// above it is refused by name. [`BlockError::BudgetExpired`] is that -/// refusal; [`BlockError::Device`] is everything else. -/// -/// `losses` is set to the disk's loss count as the device that ran the -/// operation counts it (`crate::block::BlockDevice::losses`), here and in -/// [`storage_write`] and [`storage_flush`]. -pub fn storage_read( - index: usize, - lba: u64, - count: u32, - buf: &mut [u8], - losses: &mut u64, -) -> BlockResult { - served(index, losses, |ctrl, local| ctrl.msc_read(local, lba, count, buf)) -} - -pub fn storage_write( - index: usize, - lba: u64, - count: u32, - buf: &[u8], - losses: &mut u64, -) -> BlockResult { - served(index, losses, |ctrl, local| ctrl.msc_write(local, lba, count, buf)) -} - -pub fn storage_flush(index: usize, losses: &mut u64) -> BlockResult { - served(index, losses, |ctrl, local| ctrl.msc_flush(local)) -} - -/// One operation on the machine's `index`-th disk, issued again, whole, on the -/// device that comes back if the disk is held for one. -/// -/// **What a filesystem above sees stays true.** A device that left under this -/// driver's reset answered nothing for the command it was inside — no status, -/// so no write in it was ever reported complete — and the reset ended that -/// command at the device (USB 2.0 §9.1.1: every state returns to Default). The -/// command is issued again from the caller's own buffer, whole: every CDB here -/// is idempotent, so blocks of it the device took before it left are written -/// with the same bytes, and a block it took half of is written whole. A device -/// that left owing a flush comes back counting the loss on the disk -/// ([`MscDevice::owes_a_flush`]), read into `losses` under the lock the -/// operation ran under, so the count is the one of the device that ran it. -/// -/// **The operation is one call, and its bound is the call's** -/// (`toyos_xhci::call`): opened by the first break or by finding the disk -/// held, carried across every command, the hold and the command sent again, -/// and closed here. -/// -/// **The hold only waits for a verdict.** It ends at the disk's own window -/// (`toyos_xhci::identity::RETURN_WINDOW`), past which the disk is lost and the -/// operation fails as any offline disk's does; at the caller's -/// `block::OPERATION`; or where the call's bound says, both of which answer -/// `BudgetExpired`: nothing was issued while it waited, and the caller asks -/// again above every lock. -fn served( - index: usize, - losses: &mut u64, - mut op: impl FnMut(&mut XhciController, usize) -> BlockResult, -) -> BlockResult { - let until = Operation::deadline(); - let mut call = AfterBreak::CLOSED; - // Once the disk was found held: until when this call may wait for it, which - // bounds taking the controller lock again to send the operation too. - let mut hold_ends = None; - let mut back = false; - loop { - let ran = with_disk_by(index, hold_ends, |ctrl, at| { - ctrl.after_break = call; - let done = op(ctrl, at); - call = core::mem::replace(&mut ctrl.after_break, AfterBreak::CLOSED); - if let Some(disk) = &ctrl.msc[at].disk { - *losses = disk.dev.debt.losses(); - } - (done, ctrl.msc[at].disk.is_none()) - }); - let (done, held) = match ran { - Some(Some(ran)) => ran, - // Not on any controller: held, or gone, which is the wait's to say. - Some(None) => (Err(BlockError::Device), true), - None => { - log!("usb-storage: disk {index} is back, and the controller lock was not free \ - before this call's bound ended; the operation is asked again"); - return Err(BlockError::BudgetExpired); - } - }; - if back { - log!("usb-storage: disk {index} is back, and the operation it was asked went out \ - again on it: {}", CameTo(done)); - } - if !held || done != Err(BlockError::Device) { - return done; - } - let ends = call.hold(crate::clock::nanos_since_boot(), AFTER_BREAK); - hold_ends = Some(ends); - match wait_for_return(index, until, ends) { - Returned::Back => back = true, - Returned::NotYet => { - log!("usb-storage: disk {index} {STILL_HELD}"); - return Err(BlockError::BudgetExpired); - } - Returned::Lost => return done, - } - } -} - -/// What a call that ended its hold with the disk still held says. -const STILL_HELD: &str = "is still held when this call may wait no longer; nothing was issued, \ - and the operation is asked again"; - -/// How a wait for a disk's device ended. -enum Returned { - /// The disk is served again, by the device that came back. - Back, - /// The call may wait no longer; the disk is still waited for. - NotYet, - /// The disk is not waited for: it is gone, or not back in time. - Lost, -} - -/// Wait until the disk is back, lost, or `until` or `ends` passes. -/// -/// **Only the verdict is waited for here.** Whatever arrives is enumerated and -/// bound where every hot-plugged device is, by the port machine on a CPU that -/// reaches a scheduler pass — never on this one, which spins with `IF` clear — -/// so this asks for the ports to be stepped and looks, without taking the -/// controller lock from a bind that holds it. -fn wait_for_return(index: usize, until: Deadline, ends: u64) -> Returned { - let mut asked = false; - loop { - match look_for(index) { - Some(Whereabouts::Here) => return Returned::Back, - Some(Whereabouts::Gone) => return Returned::Lost, - Some(Whereabouts::Awaited) | None => {} - } - if crate::clock::nanos_since_boot() >= ends || until.reached(crate::clock::now()) { - return Returned::NotYet; - } - ports_wanted(!core::mem::replace(&mut asked, true)); - let _ = crate::clock::settles(PORT_LOOK_NS, || false); - } -} - -/// How often a caller waiting for a disk's device looks: under the 100 ms -/// debounce a connect waits out, so the look is never what it waited on. -const PORT_LOOK_NS: u64 = 1_000_000; - -/// A block result as one word, for the line that says what a re-issue came to. -struct CameTo(BlockResult); - -impl core::fmt::Display for CameTo { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str(match self.0 { - Ok(()) => "it completed", - Err(BlockError::BudgetExpired) => "it ran out of its operation budget", - Err(BlockError::Device) => "it failed", - }) - } -} - diff --git a/kernel/src/elf/mod.rs b/kernel/src/elf/mod.rs index 436ed499124..b74935f96a8 100644 --- a/kernel/src/elf/mod.rs +++ b/kernel/src/elf/mod.rs @@ -320,7 +320,7 @@ pub(crate) fn read_backing_into( backing: &dyn crate::file_backing::FileBacking, offset: u64, dst: KernelSlice, -) -> crate::block::BlockResult { +) -> crate::file_backing::BlockResult { let mut remaining = dst.size(); let mut file_off = offset; let mut buf_off = 0usize; diff --git a/kernel/src/file_backing.rs b/kernel/src/file_backing.rs index ce46f3b8d05..c6926bb0812 100644 --- a/kernel/src/file_backing.rs +++ b/kernel/src/file_backing.rs @@ -2,7 +2,6 @@ use alloc::sync::Arc; use alloc::vec::Vec; use bcachefs::Extent; -use crate::block::BlockResult; use crate::object::shm::SharedMemObject; use crate::rootfs::MemoryImage; use toyos_abi::syscall::SyscallError; @@ -11,6 +10,22 @@ use toyos_abi::syscall::SyscallError; const BLOCK_SIZE: usize = crate::mm::PAGE_SIZE as usize; const BLOCK_SIZE_U64: u64 = crate::mm::PAGE_SIZE; +/// Why a page a backing holds could not be read. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BlockError { + /// The backing itself failed the read. + Device, +} + +/// Every failure here was attempted: the bcachefs crate retries none. +impl bcachefs::TransferError for BlockError { + fn refused_before_attempt(&self) -> bool { + false + } +} + +pub type BlockResult = Result<(), BlockError>; + /// Backing store for a memory-mapped file: ROOT's image, an image a caller /// handed over, or a tmpfs file; callers do not know which. pub trait FileBacking: Send + Sync { diff --git a/kernel/src/file_cache.rs b/kernel/src/file_cache.rs index 0d29ebfc4df..cc54a7f8718 100644 --- a/kernel/src/file_cache.rs +++ b/kernel/src/file_cache.rs @@ -3,8 +3,7 @@ use alloc::collections::btree_map::Entry; use alloc::collections::BTreeMap; use alloc::sync::Arc; -use crate::block; -use crate::file_backing::FileBacking; +use crate::file_backing::{BlockError, FileBacking}; use crate::sync::Lock; use crate::user_ptr::{ByteSource, UserBytesMut}; @@ -65,7 +64,8 @@ static FILE_CACHE: Lock = Lock::new(FileCache { /// Install the memory budget; must run after the PMM sizes RAM and before any file is opened. pub fn init() { - let max_pages = block::file_cache_pages(); + let (total, _) = crate::mm::pmm::stats(); + let max_pages = (((total / 64) / PAGE_SIZE as u64) as usize).clamp(2048, 65536); FILE_CACHE.lock().max_pages = max_pages; log!("file cache: budget {} pages ({} MiB)", max_pages, max_pages * PAGE_SIZE / (1024 * 1024)); } @@ -163,7 +163,7 @@ pub fn read_page( page_idx: u32, offset: usize, buf: &mut UserBytesMut, -) -> Result<(), block::BlockError> { +) -> Result<(), BlockError> { let backing; { let mut cache = FILE_CACHE.lock(); @@ -223,7 +223,7 @@ pub fn write_page( page_idx: u32, offset: usize, data: &S, -) -> Result<(), block::BlockError> { +) -> Result<(), BlockError> { // A resident page is written under the same lock acquisition that found it; the fetch path below drops the lock, and a sibling's eviction in that window would otherwise merge the write into a blank page. let backing; { diff --git a/kernel/src/gpt.rs b/kernel/src/gpt.rs deleted file mode 100644 index 49f1f92555f..00000000000 --- a/kernel/src/gpt.rs +++ /dev/null @@ -1,556 +0,0 @@ -//! The partitions of the disks this kernel drives — the USB disks, until usbd -//! drives the controller — and the ones the bootloader named. -//! -//! **The loader names four partitions** ([`KernelArgs`]): the ROOT it read, -//! the booted slot's volume, the log's and the ESP firmware loaded it from -//! (`Role::Boot`). This kernel mounts none of them but ROOT, which is in -//! memory (`rootfs`); it answers their GUIDs in the inventory ([`loaded`]), -//! so the log and boot file servers find theirs and the supervisor knows the -//! running slot, on whatever disk and whoever drives it. -//! -//! The boot partition's location must match firmware's account on a disk -//! [`probe`] read, and [`probe_usb_disks`] waits for the disk that carries it -//! to appear. A partition a process claims is found by [`claimable`], on the -//! disks [`probe`] read, and the inventory is answered from the tables -//! [`probe`] listed ([`inventory`]). Nothing here writes. - -use alloc::vec::Vec; - -use crate::block::{BlockDevice, DeviceId, Handle}; -use crate::device::ClaimError; -use crate::drivers::{usb_storage, xhci}; -use crate::sync::Lock; -use toyos_abi::boot::KernelArgs; -use toyos_abi::inventory::Role; -use toyos_abi::part::PartGuid; -use toyos_gpt::{GptError, Guid, Partition, Sectors}; - -/// The partition firmware loaded the bootloader from, in firmware's terms. -#[derive(Clone, Copy, Debug)] -pub struct BootPartition { - pub guid: Guid, - /// In the boot device's logical blocks; for cross-checking a GPT entry, never I/O. - pub start_lba: u64, - pub blocks: u64, -} - -/// A partition this kernel was given, as a place on a device it can read. -#[derive(Clone, Copy, Debug)] -pub struct Volume { - pub device: DeviceId, - /// The device's logical block size; both LBAs below are in these units. - pub lba_bytes: u32, - pub start_lba: u64, - pub blocks: u64, -} - -/// What the kernel knows about where the boot partition lives. `Ambiguous` -/// is permanent: two devices carrying one partition GUID means one is a -/// clone, and nothing here can tell which one firmware read. -enum Resolution { - Unknown, - Found { boot: Volume }, - Ambiguous, -} - -static FIRMWARE: Lock> = Lock::new(None); -static RESOLVED: Lock = Lock::new(Resolution::Unknown); -/// The partitions the loader named, by role; empty before [`init`]. -static LOADED: Lock> = Lock::new(Vec::new()); -/// Every device [`probe`] read, with its logical block size: the disks a -/// partition claim is looked for on. Taken alone. -static DISKS: Lock> = Lock::new(Vec::new()); - -/// A table is outside every partition, so nothing a holder writes changes it, -/// and nothing here reads a disk again to answer. -static LISTED: Lock> = Lock::new(Vec::new()); - -/// One disk's entries, as [`probe`] listed them. -struct Listed { - handle: Handle, - lba_bytes: u32, - parts: Vec, -} - -/// The most entries the inventory lists for one disk: the 128 a GPT's entry -/// array holds by convention (UEFI 2.10 §5.3.2). A disk with more says so -/// when it is probed. -const MAX_LISTED: usize = 128; - -/// A partition that is not whole blocks is held by nothing, since no view can -/// be made of it. -pub fn inventory() -> Vec<(DeviceId, Partition, Option)> { - let listed: Vec<(Handle, u32, Vec)> = LISTED - .lock() - .iter() - .map(|disk| (disk.handle.clone(), disk.lba_bytes, disk.parts.clone())) - .collect(); - let mut out = Vec::new(); - for (handle, lba_bytes, parts) in listed { - for part in parts { - let holder = match crate::block::span_blocks(part.first_lba(), part.lba_count().get(), lba_bytes) { - Ok((first_block, blocks)) => handle.holder(first_block, first_block + blocks), - Err(_) => None, - }; - out.push((handle.device_id(), part, holder)); - } - } - out -} - -/// The partitions the loader named, for the inventory. -pub fn loaded() -> Vec<(Role, Guid)> { - LOADED.lock().clone() -} - -/// Where the log partition the loader named is, as far as this kernel sees. -pub enum LogPlace { - /// The loader named none. - Unnamed, - /// On a disk this kernel drives, where its file server claims it. - Driven, - /// Not on the disk this kernel booted from and drives, which is where an - /// image carries it: this machine has no log partition. - Absent, - /// The boot disk is one this kernel does not drive, so whether the log - /// partition is on it is its file server's to say. - Undriven, -} - -/// [`LogPlace`], once [`probe_usb_disks`] has read every disk it will. -pub fn log_place() -> LogPlace { - let Some(guid) = LOADED.lock().iter().find(|(role, _)| *role == Role::Log).map(|(_, g)| *g) else { - return LogPlace::Unnamed; - }; - if LISTED.lock().iter().any(|disk| disk.parts.iter().any(|p| p.unique_guid() == guid)) { - return LogPlace::Driven; - } - match *RESOLVED.lock() { - Resolution::Found { .. } => LogPlace::Absent, - Resolution::Unknown | Resolution::Ambiguous => LogPlace::Undriven, - } -} - -/// List `handle`'s table into [`LISTED`], once per disk. -fn list(sectors: &mut DeviceSectors<'_>, handle: &Handle, lba_bytes: u32) { - let id = handle.device_id(); - let mut found = alloc::vec![None; MAX_LISTED]; - // A disk with no table this kernel parses carries no partition. - let Ok(scan) = toyos_gpt::list(sectors, &mut found) else { return }; - if scan.matched as usize > MAX_LISTED { - log!( - "gpt: device {id} carries {} partitions and the inventory lists {MAX_LISTED}", - scan.matched - ); - } - let mut parts = Vec::new(); - for entry in found.into_iter().flatten() { - match entry { - Ok(part) => parts.push(part), - Err(unplaced) => log!( - "gpt: device {id} states entry {} ({}) at LBA {}..={}, whose blocks are no \ - partition on it, and the inventory does not list it", - unplaced.index, - unplaced.unique_guid, - unplaced.first, - unplaced.last - ), - } - } - LISTED.lock().push(Listed { handle: handle.clone(), lba_bytes, parts }); -} - -/// Take the partitions the loader named out of its handoff. -pub fn init(args: &KernelArgs) { - let mut loaded = LOADED.lock(); - if args.root_partition_guid != [0; 16] { - loaded.push((Role::Root, Guid(args.root_partition_guid))); - } - if args.slot_volume_guid != [0; 16] { - loaded.push((Role::Slot, Guid(args.slot_volume_guid))); - } - if args.log_partition_guid != [0; 16] { - let log_guid = Guid(args.log_partition_guid); - log!("gpt: the boot volume names {log_guid} as the log partition"); - loaded.push((Role::Log, log_guid)); - } - if args.boot_partition_present == 0 { - log!("gpt: firmware named no boot partition — this machine has none"); - return; - } - let part = BootPartition { - guid: Guid(args.boot_partition_guid), - start_lba: args.boot_partition_start_lba, - blocks: args.boot_partition_blocks, - }; - log!( - "gpt: firmware booted us from partition {} at LBA {}+{}", - part.guid, part.start_lba, part.blocks - ); - loaded.push((Role::Boot, part.guid)); - *FIRMWARE.lock() = Some(part); -} - -pub fn boot_partition() -> Option { - *FIRMWARE.lock() -} - -/// True only while resolution is still `Unknown`; `Ambiguous` is permanent. -pub fn boot_volume_still_possible() -> bool { - matches!(*RESOLVED.lock(), Resolution::Unknown) -} - -/// Ask every USB disk for its table, retrying while the boot partition is -/// still not found: a USB-booted disk appears only once the controller binds -/// it, and a claim of one of its partitions is found on the disks read here. -pub fn probe_usb_disks() { - let deadline = crate::clock::nanos_since_boot() + xhci::PORT_SETTLE_CEILING.nanos(); - let mut probed = 0; - loop { - probed = probe_announced(probed); - // Nothing further can change: no partition named, resolved, or - // ambiguity no device can repair. - if boot_partition().is_none() || !boot_volume_still_possible() { - return; - } - if crate::clock::nanos_since_boot() >= deadline { - log!( - "usb-storage: {probed} disk(s) on this machine and none carries the boot \ - partition after {} ms of looking", - xhci::PORT_SETTLE_CEILING.duration().millis() - ); - return; - } - // Paced, not spun: one MMIO read per port under the controller lock, - // on physical hardware. - let next = crate::clock::nanos_since_boot() + xhci::PORT_POLL.nanos(); - while crate::clock::nanos_since_boot() < next { - core::hint::spin_loop(); - } - xhci::recheck_ports(); - } -} - -/// Probe every disk announced since the last call; indices are stable and -/// dense, so a disk is never probed twice. -fn probe_announced(mut probed: usize) -> usize { - let count = usb_storage::count(); - while probed < count { - let index = probed; - probed += 1; - // One call for both the handle and the block size; the disk carries its - // own geometry, and registering it is what makes it openable by number. - let Some((handle, lba_bytes)) = usb_storage::handle(index) else { - log!( - "usb-storage: disk {index} was announced and was gone again before its partition \ - table could be read — not probed" - ); - continue; - }; - probe(&handle, lba_bytes); - } - probed -} - -/// Ask one registered block device what it carries: its table for the -/// inventory, and the boot partition when firmware named one. -pub fn probe(handle: &Handle, lba_bytes: u32) { - let id = handle.device_id(); - let first = { - let mut disks = DISKS.lock(); - let first = !disks.iter().any(|(held, _)| held.device_id() == id); - if first { - disks.push((handle.clone(), lba_bytes)); - } - first - }; - let mut sectors = DeviceSectors::new(handle, lba_bytes); - if first { - list(&mut sectors, handle, lba_bytes); - } - - let Some(firmware) = boot_partition() else { - return; - }; - let found = match toyos_gpt::locate(&mut sectors, firmware.guid) { - Ok(found) => found, - Err(GptError::NotFound { used_entries }) => { - log!("gpt: device {id} has {used_entries} partitions and none of them is ours"); - return; - } - Err(e) => { - log!("gpt: device {id} has no partition table we can use: {e:?}"); - return; - } - }; - - // Firmware's and the table's accounts must agree; a mismatch refuses, never repairs. - let part = found.partition(); - if part.first_lba() != firmware.start_lba || part.lba_count().get() != firmware.blocks { - log!( - "gpt: device {id} puts {} at LBA {}+{} but firmware said {}+{} — not treating it as \ - the boot volume", - part.unique_guid(), - part.first_lba(), - part.lba_count(), - firmware.start_lba, - firmware.blocks - ); - return; - } - - let volume = Volume { - device: id, - lba_bytes, - start_lba: part.first_lba(), - blocks: part.lba_count().get(), - }; - - let mut resolved = RESOLVED.lock(); - match *resolved { - Resolution::Unknown => { - log!( - "gpt: device {id} carries the boot partition at LBA {}+{} ({}-byte blocks), \ - entry {} of {} on disk {}{}", - volume.start_lba, - volume.blocks, - lba_bytes, - part.index(), - found.used_entries(), - found.disk_guid(), - if part.is_efi_system() { "" } else { " — and its type is not ESP" } - ); - *resolved = Resolution::Found { boot: volume }; - } - Resolution::Found { boot: first, .. } => { - log!( - "gpt: device {id} carries the same partition GUID as device {} — one of them is \ - a copy and nothing here can say which one we booted from, so this machine now \ - has no boot volume", - first.device - ); - *resolved = Resolution::Ambiguous; - } - Resolution::Ambiguous => { - log!("gpt: device {id} also carries the boot partition GUID"); - } - } -} - -/// One partition a claim may hold: where it is, and its unique GUID. -#[derive(Clone, Copy, Debug)] -pub struct Claimable { - pub volume: Volume, - pub unique: Guid, -} - -/// Partitions no claim may take whatever a table says: ROOT's source, when -/// the boot could not hold its span (`rootfs::hold_source`). -static WITHHELD: Lock> = Lock::new(Vec::new()); - -/// Refuse every claim of `guid` for the machine's life, as the kernel's. -pub fn withhold(guid: PartGuid) { - WITHHELD.lock().push(Guid(guid.0)); -} - -/// Where one partition is on the disks that answered, and which did not. -pub struct Sought { - /// The one partition carrying the GUID, `None` where no table that - /// answered carries it, or why the tables that answered name no one. - pub found: Result, Unnamed>, - /// The disks that did not answer a read of their table, of which neither - /// "none" nor "one" is known. - pub silent: Vec, -} - -/// Why the tables that answered name no one partition for a GUID. -#[derive(Clone, Copy, Debug)] -pub enum Unnamed { - /// Carried twice, on one disk or across two. - Ambiguous, - /// Named by a table that refuses it. - Unusable, -} - -impl From for ClaimError { - fn from(unnamed: Unnamed) -> Self { - match unnamed { - Unnamed::Ambiguous => ClaimError::Ambiguous, - Unnamed::Unusable => ClaimError::Unusable, - } - } -} - -/// The one partition on this machine whose unique GUID is `guid`, past the -/// range and overlap checks `toyos_gpt::locate` makes (UEFI 2.10 §5.3.3). -/// -/// Read off the disks when asked and never cached: a table is outside every -/// partition, so no claim can write one. `Absent` for a GUID no table carries -/// and for the zero GUID, which GPT gives every unused entry; `Ambiguous` for -/// one carried twice, on one disk or across two; `Unusable` for a disk that -/// did not answer, since then neither "none" nor "one" is known; -/// `KernelDriven` for a GUID [`withhold`] named. -pub fn claimable(guid: PartGuid) -> Result { - let target = Guid(guid.0); - if target.is_zero() { - return Err(ClaimError::Absent); - } - if WITHHELD.lock().contains(&target) { - log!("partclaim: {target} is where ROOT was read from, and the kernel withholds it"); - return Err(ClaimError::KernelDriven); - } - let sought = seek(guid); - let found = sought.found?; - if !sought.silent.is_empty() { - return Err(ClaimError::Unusable); - } - found.ok_or(ClaimError::Absent) -} - -/// Look for `guid` on every disk [`probe`] read, reading past a disk that does -/// not answer and naming it. -pub fn seek(guid: PartGuid) -> Sought { - let target = Guid(guid.0); - let mut silent = Vec::new(); - if target.is_zero() { - return Sought { found: Ok(None), silent }; - } - let disks = DISKS.lock().clone(); - let mut found: Option = None; - for (handle, lba_bytes) in &disks { - let id = handle.device_id(); - let part = match toyos_gpt::locate(&mut DeviceSectors::new(handle, *lba_bytes), target) { - Ok(located) => located.partition(), - Err(e) => { - match table_refused(id, target, e) { - Ok(Unread::Lacks) => {} - Ok(Unread::Silent) => silent.push(id), - Err(refused) => return Sought { found: Err(refused), silent }, - } - continue; - } - }; - if let Some(first) = found { - log!( - "partclaim: {target} is on device {} and on device {id}, so it names no one \ - partition", - first.volume.device - ); - return Sought { found: Err(Unnamed::Ambiguous), silent }; - } - found = Some(Claimable { - volume: Volume { - device: id, - lba_bytes: *lba_bytes, - start_lba: part.first_lba(), - blocks: part.lba_count().get(), - }, - unique: part.unique_guid(), - }); - } - Sought { found: Ok(found), silent } -} - -/// A disk whose table gave no partition for a GUID, and no refusal. -enum Unread { - /// Its table does not carry the GUID. - Lacks, - /// It did not answer a read of its table. - Silent, -} - -/// What a table's refusal means for a claim: a disk that does not carry the -/// partition, one that did not answer, or the claim's refusal. -fn table_refused(id: DeviceId, target: Guid, e: GptError) -> Result { - match e { - GptError::NotFound { .. } => Ok(Unread::Lacks), - GptError::ReadFailed(lba) => { - log!("partclaim: device {id} did not answer a read of LBA {lba} while looking for {target}"); - Ok(Unread::Silent) - } - GptError::DuplicateUniqueGuid { first, second } => { - log!("partclaim: device {id} carries {target} in entries {first} and {second}"); - Err(Unnamed::Ambiguous) - } - GptError::PartitionRange { .. } | GptError::PartitionOverlap { .. } => { - log!("partclaim: device {id} names {target} and its own table refuses it: {e:?}"); - Err(Unnamed::Unusable) - } - // No table this kernel parses: a disk that carries no partition, which - // is what `probe` concluded of it too. - GptError::UnsupportedLbaSize(_) - | GptError::DeviceTooSmall(_) - | GptError::NoProtectiveMbr - | GptError::NoHeader - | GptError::UnsupportedRevision(_) - | GptError::HeaderSize(_) - | GptError::HeaderReserved(_) - | GptError::HeaderMisplaced(_) - | GptError::HeaderCrc { .. } - | GptError::UsableRange { .. } - | GptError::EntrySize(_) - | GptError::EntryArrayTooBig { .. } - | GptError::EntryArrayMisplaced { .. } - | GptError::EntryArrayCrc { .. } - | GptError::UsableRangeCoversBackup { .. } => Ok(Unread::Lacks), - } -} - - -/// The kernel's 4 KiB `BlockDevice`, seen in the device's own logical blocks; caches one block. -struct DeviceSectors<'a> { - dev: &'a Handle, - lba_bytes: u32, - lbas_per_block: u64, - cached: Option, - buf: [u8; 4096], -} - -impl<'a> DeviceSectors<'a> { - fn new(dev: &'a Handle, lba_bytes: u32) -> Self { - // Zero when lba_bytes doesn't divide 4096; that fails every read cleanly. - let lbas_per_block = if lba_bytes != 0 && 4096 % lba_bytes == 0 { - (4096 / lba_bytes) as u64 - } else { - 0 - }; - Self { dev, lba_bytes, lbas_per_block, cached: None, buf: [0; 4096] } - } -} - -impl Sectors for DeviceSectors<'_> { - fn lba_bytes(&self) -> u32 { - self.lba_bytes - } - - fn lba_count(&self) -> u64 { - self.dev.block_count().saturating_mul(self.lbas_per_block) - } - - fn lba_count_granularity(&self) -> core::num::NonZeroU64 { - core::num::NonZeroU64::new(self.lbas_per_block) - .expect("a supported GPT LBA size divides 4096") - } - - fn read_lba(&mut self, lba: u64, out: &mut [u8]) -> bool { - if self.lbas_per_block == 0 || out.len() != self.lba_bytes as usize { - return false; - } - let block = lba / self.lbas_per_block; - if block >= self.dev.block_count() { - return false; - } - if self.cached != Some(block) { - // Must clear `cached` on a failed read too, or the buffer's previous block - // looks valid for the next LBA in this block. - if self.dev.lock().read_blocks(block, 1, &mut self.buf).is_err() { - self.cached = None; - return false; - } - self.cached = Some(block); - } - let at = (lba % self.lbas_per_block) as usize * self.lba_bytes as usize; - out.copy_from_slice(&self.buf[at..at + out.len()]); - true - } -} diff --git a/kernel/src/inventory.rs b/kernel/src/inventory.rs index 8a907e325b4..3298b8efdcf 100644 --- a/kernel/src/inventory.rs +++ b/kernel/src/inventory.rs @@ -2,10 +2,8 @@ //! `SYS_DEVICE_INVENTORY` answers. //! //! **Assembled from what each subsystem already keeps**: the PCI functions and -//! their identities are `pcidev`'s from enumeration, the USB devices are the -//! ones the xHCI driver bound, and the block devices are the ones registered. -//! A partition's state is the block layer's hold on exactly its span, the -//! record every view is refused against. +//! their identities are `pcidev`'s from enumeration, and the partitions the +//! loader named are `loaded`'s. //! //! **A holder is found where its handle is**: every process's table is walked //! for device claims, one table at a time and with the process table dropped @@ -14,42 +12,17 @@ use alloc::sync::Arc; use alloc::vec::Vec; -use toyos_abi::inventory::{Block, Claim, Claimed, Holder, Loaded, PartState, Partition, Record, NAME_BYTES}; +use toyos_abi::inventory::{Claim, Claimed, Holder, Loaded, Record, NAME_BYTES}; use crate::object::KObjectRef; use crate::process; -/// Every record, in a fixed order: PCI, USB, block devices, partitions, -/// claims, and the partitions the loader named. +/// Every record, in a fixed order: PCI, claims, and the partitions the loader +/// named. pub fn collect() -> Vec { let mut out: Vec = crate::pcidev::inventory().into_iter().map(Record::Pci).collect(); - out.extend(crate::drivers::xhci::inventory().into_iter().map(Record::Usb)); - out.extend( - crate::block::registered() - .iter() - .map(|h| Record::Block(Block { device: h.device_id(), blocks: h.block_count() })), - ); - for (device, part, holder) in crate::gpt::inventory() { - out.push(Record::Partition(Partition { - device, - index: part.index(), - type_guid: part.type_guid().0, - unique_guid: part.unique_guid().0, - first_lba: part.first_lba(), - lbas: part.lba_count().get(), - state: match holder { - None => PartState::Free, - Some(crate::block::Holder::Kernel(_)) => PartState::Kernel, - Some(crate::block::Holder::Claim(_)) => PartState::Claimed, - }, - })); - } out.extend(claims().into_iter().map(Record::Claim)); - out.extend( - crate::gpt::loaded() - .into_iter() - .map(|(role, guid)| Record::Loaded(Loaded { role, unique_guid: guid.0 })), - ); + out.extend(crate::loaded::loaded().into_iter().map(|(role, unique_guid)| Record::Loaded(Loaded { role, unique_guid }))); out } @@ -72,10 +45,6 @@ fn claims() -> Vec { // A function the last handle has let go names nothing. let Some(at) = claim.pci(crate::pcidev::held_at) else { continue }; Claimed::Pci(at) - } else if claim.class() == toyos_abi::syscall::DeviceType::Partition { - // A partition the last handle has let go names nothing. - let Some((device, unique_guid)) = claim.partition_on() else { continue }; - Claimed::Partition { device, unique_guid } } else { Claimed::Class(claim.class()) }; diff --git a/kernel/src/iommu/mod.rs b/kernel/src/iommu/mod.rs index 8821b14cae5..96397140197 100644 --- a/kernel/src/iommu/mod.rs +++ b/kernel/src/iommu/mod.rs @@ -295,11 +295,6 @@ pub fn drivers_settled() { /// Why a source could not be given a message: the architecture's own reasons. pub use messages::Refused; -pub struct MsiMessage { - pub address: u32, - pub data: u32, -} - /// Every claimed function's message is translated to its own interrupt, and /// no other's. Only [`remapping`] makes one. #[derive(Clone, Copy)] @@ -387,19 +382,6 @@ pub fn claim_msi( Ok(Remapped { claimed, function: *function }) } -/// The message a kernel driver's `bus:device.function` raises `irq` with. -/// Takes the triple, not a [`StreamId`]: what a requester id is stays in this -/// module. -pub fn driver_msi( - bus: u8, - device: u8, - function: u8, - irq: crate::arch::DriverIrq, -) -> Result { - messages::msi(StreamId::pci(bus, device, function), irq) - .map(|msi| MsiMessage { address: msi.address, data: msi.data }) -} - /// Reached from the IDT gate the unit's own `FEDATA` names. /// /// Fires when a device has been told no, so what it reports is a bug in whoever owns that device, not in the IOMMU. diff --git a/kernel/src/irq_census.rs b/kernel/src/irq_census.rs index 7f300ce711f..df075624e26 100644 --- a/kernel/src/irq_census.rs +++ b/kernel/src/irq_census.rs @@ -18,8 +18,6 @@ pub enum Source { Timer, /// Vector 0xFC, `irqchip::kick_cpu`'s IPI. Kick, - /// Vector 0x21, xHCI MSI-X (or MSI). - Xhci, /// Vectors 0x28-0x2C: the MSI-X of a PCI function a process drives, and /// the lines of an ISA function one does. One count for all five: which /// claim an interrupt belonged to is the claim's own record, and the census @@ -40,12 +38,11 @@ pub enum Source { } impl Source { - pub const COUNT: usize = 9; + pub const COUNT: usize = 8; /// 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", "dmafault", "tlb", "nmi", "spurious", - "unclaimed", + "timer", "kick", "userdev", "dmafault", "tlb", "nmi", "spurious", "unclaimed", ]; } diff --git a/kernel/src/irq_ring.rs b/kernel/src/irq_ring.rs deleted file mode 100644 index 04680066221..00000000000 --- a/kernel/src/irq_ring.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Per-CPU IRQ event records: ISRs stamp `(cpu, source)` with -//! `nanos_since_boot` and set `need_resched`; the next scheduler entry -//! on that CPU drains it into wakes. One coalescing slot per -//! `(cpu, source)`, 0 meaning empty and never a real timestamp. -//! Same-CPU only (indexed by `percpu::cpu_id()`), so Relaxed suffices. - -use core::sync::atomic::{AtomicU64, Ordering}; - - -/// Interrupt sources that drive scheduling; exhaustive, so a new variant requires updating every `match`. -/// The discriminant is `trace::Kind::IrqDrain`'s top byte. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum IrqSource { - Xhci = 2, -} - -impl IrqSource { - pub const COUNT: usize = 1; - - /// This source's record in a CPU's slots. - const fn slot(self) -> usize { - match self { - Self::Xhci => 0, - } - } -} - -/// A CPU's slot per source, written by that CPU's handlers and taken by that CPU. -pub struct Local([AtomicU64; IrqSource::COUNT]); - -impl Local { - pub fn new() -> Self { - Self([const { AtomicU64::new(0) }; IrqSource::COUNT]) - } -} - -fn slot(source: IrqSource) -> &'static AtomicU64 { - &crate::cpu_local::here().irq_ring.0[source.slot()] -} - -/// Records an IRQ on the current CPU (ISR-only); coalesces into the earlier timestamp. -pub fn isr_publish(source: IrqSource, timestamp_nanos: u64) { - // MSI-X vectors are configured after clock calibration, so a real IRQ never stamps 0. - assert!(timestamp_nanos != 0, "irq_ring: zero IRQ timestamp"); - let slot = slot(source); - // ISRs run with IF=0, so this load-then-store can't interleave with a same-CPU `take`. - if slot.load(Ordering::Relaxed) == 0 { - slot.store(timestamp_nanos, Ordering::Relaxed); - } -} - -/// Consumes the current CPU's pending record for `source`, returning its IRQ-time timestamp. -pub fn take(source: IrqSource) -> Option { - let slot = slot(source); - // Atomic swap: an interrupting ISR sees either the old record or the cleared slot, never a torn value. - match slot.swap(0, Ordering::Relaxed) { - 0 => None, - ts => { - let latency_us = crate::clock::nanos_since_boot().saturating_sub(ts) / 1_000; - crate::trace::trace_irq_drain(source, latency_us); - Some(ts) - } - } -} - -/// True if `source` has an undrained record on this CPU; non-consuming, unlike [`take`]. -pub fn pending(source: IrqSource) -> bool { - slot(source).load(Ordering::Relaxed) != 0 -} diff --git a/kernel/src/keyboard.rs b/kernel/src/keyboard.rs deleted file mode 100644 index d91b0ffc894..00000000000 --- a/kernel/src/keyboard.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! 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_RELEASED}; - -static KEY_BUF: Lock> = Lock::new(VecDeque::new()); - -/// 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; - -/// 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(); -} - -/// 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 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 -} - -pub fn has_data() -> bool { - !KEY_BUF.lock().is_empty() -} - -pub fn try_read_event() -> Option { - KEY_BUF.lock().pop_front() -} diff --git a/kernel/src/loaded.rs b/kernel/src/loaded.rs new file mode 100644 index 00000000000..2cc6a4ec82a --- /dev/null +++ b/kernel/src/loaded.rs @@ -0,0 +1,29 @@ +//! The partitions the loader named: the ROOT it read, the booted slot's +//! volume, the log's, and the ESP firmware loaded it from (`Role::Boot`). +//! This kernel reads none of them but ROOT, which is in memory (`rootfs`); it +//! answers their GUIDs in the inventory, so the log and boot file servers find +//! theirs and the supervisor knows the running slot, whoever drives the disk. + +use alloc::vec::Vec; + +use toyos_abi::boot::KernelArgs; +use toyos_abi::inventory::Role; + +use crate::sync::Lock; + +static LOADED: Lock> = Lock::new(Vec::new()); + +/// Take the partitions the loader named out of its handoff. +pub fn init(args: &KernelArgs) { + let named = [ + (Role::Root, args.root_partition_guid), + (Role::Slot, args.slot_volume_guid), + (Role::Log, args.log_partition_guid), + (Role::Boot, if args.boot_partition_present == 0 { [0; 16] } else { args.boot_partition_guid }), + ]; + *LOADED.lock() = named.into_iter().filter(|(_, guid)| *guid != [0; 16]).collect(); +} + +pub fn loaded() -> Vec<(Role, [u8; 16])> { + LOADED.lock().clone() +} diff --git a/kernel/src/log/mod.rs b/kernel/src/log/mod.rs index 62b814ce1a1..291b2d3a7ac 100644 --- a/kernel/src/log/mod.rs +++ b/kernel/src/log/mod.rs @@ -8,7 +8,6 @@ pub mod console; mod handoff; pub mod read; -pub mod recovery; pub mod registry; pub mod shard; mod stamp; diff --git a/kernel/src/log/recovery.rs b/kernel/src/log/recovery.rs deleted file mode 100644 index 0b1629053e7..00000000000 --- a/kernel/src/log/recovery.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! The black box's recovery section: the USB stack's records from the boot's -//! first transport break on, read off the ring for whichever seal is ending -//! this boot. -//! -//! Which records, how many and where they go are `toyos_blackbox::Recovery`'s -//! and `Kept`'s; this walks the ring for them. **Lock-free and allocation-free, -//! because two of its three callers are the panic path and an interrupt entry -//! on a wedged machine**: [`read::snapshot_committed`] is the one reader either -//! may use. - -use toyos_abi::log::LogRecord; -use toyos_blackbox::{Kept, Recovery, Report}; - -use super::read::{self, RecordSink}; -use super::LogStamp; - -/// Write the section at `report`'s end. -pub fn seal_into(report: &mut Report<'_>) { - struct Measure(Recovery); - impl RecordSink for Measure { - fn put(&mut self, record: &LogRecord) -> bool { - self.0.saw(record.at_ns, record.message(), record); - true - } - } - struct Place<'k, 'r, 'a>(&'k mut Kept<'r, 'a>); - impl RecordSink for Place<'_, '_, '_> { - fn put(&mut self, record: &LogRecord) -> bool { - self.0.put(record.message(), record) - } - } - - // One upper bound for both walks: a record committed between them is - // stamped past it, so the second walk places what the first measured. - let to = read::newest_committed(); - let mut measure = Measure(Recovery::new()); - read::snapshot_committed(LogStamp::ZERO, to, &mut measure, &super::SEALING); - let mut kept = report.recovery(measure.0); - if let Some(from) = kept.from() { - read::snapshot_committed(LogStamp::since_zero(from), to, &mut Place(&mut kept), &super::SEALING); - } - kept.close(); -} diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 8f1019ac7bd..66250531908 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -42,15 +42,8 @@ mod panic; mod panic_reboot; mod power; -mod keyboard; -mod mouse; -#[cfg(feature = "boot-actuators")] -mod usb_gate; -#[cfg(feature = "boot-actuators")] -mod sched_gate; -mod block; -mod gpt; mod inventory; +mod loaded; mod rollback; mod rootfs; mod file_cache; @@ -76,7 +69,6 @@ mod census; mod irq_census; #[cfg(feature = "mask-windows")] mod windows; -mod irq_ring; mod trace; mod time; mod clock; @@ -108,7 +100,7 @@ mod late_panic { use alloc::boxed::Box; use arch::{cpu, percpu}; -use drivers::{acpi, gop, pci, serial, virtio_console, xhci}; +use drivers::{acpi, gop, pci, serial, virtio_console}; use toyos_abi::boot::{KernelArgs, MemoryMapEntry}; use toyos_rootimage::handoff::{held, Descriptor}; @@ -204,29 +196,11 @@ fn report_power_on(args: &KernelArgs, complete: u64) { /// Says where this boot's log can be read, on the last surface still showing it once userland owns the screen. fn report_log_destination() { - // Kernel-side because panic_console owns the panel; logkeeper reports which file it opened separately. - // Whether the partition is on a disk this kernel reads, not whether its file server mounted it: - // that server says so itself, and this kernel mounts nothing but ROOT. - let console = drivers::serial::has_console(); - let has_log = match gpt::log_place() { - gpt::LogPlace::Driven => true, - gpt::LogPlace::Unnamed | gpt::LogPlace::Absent => false, - gpt::LogPlace::Undriven => { - log!("log: /log is on a disk this kernel does not drive; its file server says whether it mounted"); - return; - } - }; + // Kernel-side because panic_console owns the panel; `/log` is its file server's, which says whether it mounted. // ASCII only: the panel's font renders anything outside 0x20..=0x7E as a dot. - match (console, has_log) { - (true, true) => log!("log: this boot is on the console and on /log"), - (false, true) => log!("log: no serial console - this boot is on /log and on the screen"), - // alert! reddens the panel's Level for exactly the two states that leave no account of this boot anywhere. - (true, false) => { - alert!("log: no /log - this boot is on the console only, and nothing outlives the power") - } - (false, false) => { - alert!("log: no serial console and no /log - this boot is on this screen and nowhere else") - } + match drivers::serial::has_console() { + true => log!("log: this boot is on the console, and on /log once its file server mounts it"), + false => log!("log: no serial console - this boot is on the screen, and on /log once its file server mounts it"), } } @@ -459,7 +433,7 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { // boot, and a wedge down there is the reason to have one. arch::watchdog::init(&pci_devices); file_cache::init(); - gpt::init(kernel_args); + loaded::init(kernel_args); boot_phase!("peripherals ready", t_periph); @@ -469,11 +443,6 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { vfs::init(); process::init(); scheduler::init(); - // Task-less half of the operation-nesting gate: this boot phase has no current task, so it establishes into the per-CPU slot. - #[cfg(feature = "boot-actuators")] - if actuator::sched_operation_nesting() { - sched_gate::run("boot"); - } pipe::init(); // ROOT is the loader's image in memory, so nothing from here to the supervisor's @@ -493,30 +462,11 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { let pid = process::spawn_supervisor(); log!("spawned {} pid={pid}", process::SUPERVISOR_PATH); - // The proof the boot up to here needed no disk: ROOT and the supervisor's image both - // came out of memory. - log!("{} {}", rootfs::SUPERVISOR_WITHOUT_A_DISK, block::census::commands_issued()); - - // After the supervisor's spawn and before it runs: nothing runs a task until - // `smp::set_ready` below. This kernel mounts no disk: `/apps`, `/config`, - // `/home`, `/state`, `/log` and `/boot` are file servers' (`/system/bin/fileserver`), - // and an NVMe controller is `/system/bin/diskserver`'s. The USB disks are still - // this kernel's, served to their file servers as partition claims, until - // usbd drives the controller. - let t_storage = clock::nanos_since_boot(); - - xhci::init(&pci_devices); - // After xhci::init: a USB-booted disk doesn't exist until the controller binds it. - gpt::probe_usb_disks(); - rootfs::hold_source(); - #[cfg(feature = "boot-actuators")] if actuator::revoked_backing_selftest() { revoke_selftest::run(); } - boot_phase!("storage ready", t_storage); - let t_devices = clock::nanos_since_boot(); // First in the device phase, after storage: its lines are the diagnostic diff --git a/kernel/src/mm/dma.rs b/kernel/src/mm/dma.rs index 66547e1ee22..088aa7e8c51 100644 --- a/kernel/src/mm/dma.rs +++ b/kernel/src/mm/dma.rs @@ -58,12 +58,6 @@ impl<'pool, D: Discipline> Dma<'pool, D> { Self { base, device, size, pool: PhantomData, how: PhantomData } } - /// How many bytes this view covers. - #[inline] - pub fn size(self) -> usize { - self.size - } - /// The address to program a device with for the first byte; never a /// physical address once a unit translates this pool's domain. #[inline] @@ -80,24 +74,6 @@ impl<'pool, D: Discipline> Dma<'pool, D> { DirectMap::phys_of(self.base) } - /// This view's base as a number, to be handed back to [`Self::from_addr`]. - #[inline] - pub fn addr(self) -> u64 { - self.base as u64 - } - - /// Rebuild a view from an [`addr`](Self::addr), a - /// [`device_addr`](Self::device_addr) and a [`size`](Self::size) taken off a - /// live one. - /// - /// # Safety - /// The three must be one live view's own, over a pool that lives for the - /// machine's life. - #[inline] - pub unsafe fn from_addr(addr: u64, device: u64, size: usize) -> Self { - Self::new(addr as *mut u8, device, size) - } - /// The `size` bytes at `offset`, refused if not wholly inside `self`. #[inline] pub fn subview(self, offset: usize, size: usize) -> Self { @@ -121,14 +97,6 @@ impl<'pool, D: Discipline> Dma<'pool, D> { unsafe { copy_nonoverlapping(src.as_ptr(), self.base.add(offset), src.len()) } } - /// Copy `dst.len()` bytes at `offset` into `dst`, never a borrow into DMA memory. - #[inline] - pub fn copy_to(self, offset: usize, dst: &mut [u8]) { - self.check(offset, dst.len()); - // SAFETY: `check` bounded the source for `dst.len()` bytes; the two cannot overlap, as `copy_from` explains. - unsafe { copy_nonoverlapping(self.base.add(offset), dst.as_mut_ptr(), dst.len()) } - } - // Panics, not `Result`: every offset/length here is the driver's own arithmetic, never a device-chosen number (those are bounded at their decode before becoming an offset). #[inline] fn check(self, offset: usize, len: usize) { @@ -227,7 +195,9 @@ impl DmaPool { Ok(Self { pages, base, size, space, device }) } - /// The whole pool as a view, borrowed so it cannot outlive `self`. + /// The whole pool as a view, borrowed so it cannot outlive `self`: the + /// virtio parse selftest's alone. + #[cfg(feature = "boot-actuators")] pub fn view(&self) -> Dma<'_> { Dma::new(self.base.as_mut_ptr(), self.device, self.size) } diff --git a/kernel/src/mm/mmio.rs b/kernel/src/mm/mmio.rs index fa0617636d9..9c6bd43375e 100644 --- a/kernel/src/mm/mmio.rs +++ b/kernel/src/mm/mmio.rs @@ -59,20 +59,6 @@ impl Mmio { self.size } - /// Rebuild a window from an [`addr`](Self::addr) and a [`size`](Self::size) - /// taken off a live one. - /// - /// For the one caller that has to keep the *numbers* and not the object: the - /// reset-time xHCI stop reads its controllers out of atomics, because a - /// panicked CPU may take no lock. - /// - /// # Safety - /// `addr` and `size` must be one live `Mmio`'s own `addr()` and `size()`, - /// over a window mapped for the machine's life. - pub unsafe fn from_addr(addr: u64, size: u64) -> Self { - Self { base: addr as *mut u8, size } - } - pub fn subregion(self, offset: u64, size: u64) -> Mmio { assert!(offset + size <= self.size, "Mmio subregion OOB: offset={:#x} size={:#x} total={:#x}", offset, size, self.size); diff --git a/kernel/src/mouse.rs b/kernel/src/mouse.rs deleted file mode 100644 index 3e977b701d2..00000000000 --- a/kernel/src/mouse.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! 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 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()); - -/// How many pointer updates the kernel holds for a reader that is not reading. -pub const MAX_QUEUED_EVENTS: usize = 512; -/// What a pointer poll waits on; no read blocks on the mouse. -pub static WATCH: Watch = Watch::new(); - -/// Which physical pointer a report came from, keyed by device and not by bus. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct PointerSource(u8); - -static IN_USE: Sources = Sources::new(); - -impl PointerSource { - /// The lowest free source, or `None` when every one is held; a caller that gets `None` must not bind the device, or two devices' buttons share one record. - pub fn claim() -> Option { - IN_USE.claim().map(Self) - } - - /// What the records of this source carry as theirs. - pub fn id(self) -> u8 { - self.0 - } -} - -/// Where a report puts the pointer. -#[derive(Clone, Copy, Debug)] -pub enum Motion { - Relative { dx: i16, dy: i16 }, - /// Each axis at most `toyos_abi::input::POINTER_ABSOLUTE_MAX`. - Absolute { x: u16, y: 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), - }; - let mut buf = MOUSE_BUF.lock(); - if buf.len() >= MAX_QUEUED_EVENTS { - buf.pop_front(); - } - buf.push_back(MouseEvent { source: source.0, buttons, scroll, kind, x, y }); -} - -/// Throw away everything queued, for the reason [`crate::keyboard::discard_queued`] gives. -pub fn discard_queued() { - MOUSE_BUF.lock().clear(); -} - -/// `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 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 { - !MOUSE_BUF.lock().is_empty() -} - -pub fn try_read_event() -> Option { - MOUSE_BUF.lock().pop_front() -} diff --git a/kernel/src/object/device.rs b/kernel/src/object/device.rs index 78805910b54..f39297015c5 100644 --- a/kernel/src/object/device.rs +++ b/kernel/src/object/device.rs @@ -17,16 +17,11 @@ use super::{Held, KObjectRef, KObjectVariant, ObjectCore, ZeroHandles}; /// What the class answers when its holder reads it. pub enum DeviceInfo { - // Keyboard and mouse answer with events, not a description. - Events, /// The scanout, whose handle the mint installs. Framebuffer(FramebufferInfo, Arc), /// The function. No buffer handle beside it: a PCI claimant asks for its /// own memory, so this mint installs nothing. PciFunction(toyos_abi::pci::PciFunctionInfo), - /// Which partition, how long, and both its GUIDs; the view it moves blocks - /// through is the claim's own (`device::Claim::partition`). - Partition(toyos_abi::part::PartitionInfo), /// The ports and lines granted, as the selector named them. Isa(toyos_abi::syscall::IsaId), /// The fixed hardware's blocks. @@ -48,7 +43,6 @@ impl DeviceInfo { /// read mints again. fn mint(&self, table: &mut HandleTable) -> Result, SyscallError> { Ok(match self { - Self::Events => Box::new([]), Self::Framebuffer(info, scanout) => { let mut info = *info; info.scanout = install_buffer(table, scanout)?; @@ -57,7 +51,6 @@ impl DeviceInfo { // Nothing to install: every address in it is a size, and the memory // is what `SYS_DEVICE_DMA_ALLOC` answers later. Self::PciFunction(info) => bytes(info).into(), - Self::Partition(info) => bytes(info).into(), Self::Isa(set) => set.wire().iter().flat_map(|word| word.to_ne_bytes()).collect(), Self::Acpi(info) => bytes(info).into(), }) @@ -125,28 +118,6 @@ impl DeviceClaim { } } - /// The view a partition claim transfers through: `None` for a claim on - /// anything else, and once the last handle has let the partition go. - /// - /// A clone, and a clone holds the partition for as long as it lives — so - /// one is kept across a device operation and never across a wait, where a - /// killed thread's stack could strand it. - pub fn partition_view(&self) -> Option { - self.reference.with(|claim| claim.partition().cloned()).flatten() - } - - /// The block device and unique GUID of the partition a partition claim is - /// on; `None` for a claim on anything else, and once the last handle has - /// let the partition go. - pub fn partition_on(&self) -> Option<(crate::block::DeviceId, [u8; 16])> { - let unique = match &self.described.lock().info { - DeviceInfo::Partition(info) => info.unique_guid, - _ => return None, - }; - let device = self.reference.with(|claim| claim.partition().map(|view| view.device_id())).flatten()?; - Some((device, unique)) - } - pub fn info_read(&self) -> bool { self.info_read.load(Ordering::Relaxed) } diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs index d2dc509d7d9..21b871c3d07 100644 --- a/kernel/src/object/ops.rs +++ b/kernel/src/object/ops.rs @@ -13,14 +13,13 @@ use kernel::sched::task::WaitClass; use crate::drivers::serial; use crate::file_cache; -use crate::time::Deadline; use crate::pipe::{self, PipeId}; use kernel::pipe::End; use crate::process::PipeMap; use crate::user_ptr::{UserBytes, UserBytesMut}; use crate::inbox::PollEntry; use crate::watch::Watch; -use crate::{device as device_registry, keyboard, mouse}; +use crate::device as device_registry; use super::device::DeviceClaim; use super::file::{FileObject, OpenFileState}; @@ -186,8 +185,7 @@ pub fn close( } } } - // Only a watch this handle's object ends is answered: one it shares (every `Console` and a - // `Device(Keyboard)` name the keyboard's) cannot cancel another's poll. + // Only a watch this handle's object ends is answered: one it shares cannot cancel another's poll. if close_ends_polls(&object) { for watch in [read_watch(&object), write_watch(&object), pipe_end_watch(&object)].into_iter().flatten() { watch.cancel_polls(); @@ -278,16 +276,14 @@ pub fn read_watch(object: &KObjectRef) -> Option { KObjectRef::Connection(c) => pipe::read_watch(c.rx()).map(WatchRef::Shared), KObjectRef::Acceptor(a) => Some(WatchRef::Shared(a.watch().clone())), KObjectRef::Process(p) => Some(WatchRef::Shared(p.watch().clone())), - KObjectRef::Console(_) => Some(WatchRef::Static(&keyboard::WATCH)), + // Nothing posts for a serial byte, so a console's readable poll has + // no source (`issues/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md`). + KObjectRef::Console(_) => None, KObjectRef::Device(d) => match d.class() { - device_registry::DeviceType::Keyboard => Some(WatchRef::Static(&keyboard::WATCH)), - device_registry::DeviceType::Mouse => Some(WatchRef::Static(&mouse::WATCH)), device_registry::DeviceType::PciFunction | device_registry::DeviceType::Isa | device_registry::DeviceType::Acpi => Some(WatchRef::Claim(d.clone())), device_registry::DeviceType::Framebuffer => None, - // A partition answers its description and has nothing to wait for. - device_registry::DeviceType::Partition => None, }, // Named unconditionally: the watch alone cannot enforce rights. KObjectRef::SysCap(_) => Some(WatchRef::Static(&crate::log::user::WATCH)), @@ -339,9 +335,8 @@ pub fn other_end_gone(object: &KObjectRef) -> bool { /// Whether closing one handle to this object ends what its watches watch, so /// every poll on them — in any ring — is answered as gone. `false` for the log -/// and the keyboard, which the machine ends on its own and which other handles -/// share: a console closing is not every console's keyboard going away. `false` -/// for a process, which only its own end ends: closing one handle to it ends no +/// and a console, which the machine ends on its own and which other handles +/// share. `false` for a process, which only its own end ends: closing one handle to it ends no /// other's watch. `false` for a claim on a function or a row: its one handle's /// close is its release, which answers every poll on the watch before the slot /// or the row can be held again (`pcidev`'s and `isa::Row`'s release). @@ -351,13 +346,10 @@ fn close_ends_polls(object: &KObjectRef) -> bool { KObjectRef::Console(_) => false, KObjectRef::Process(_) => false, KObjectRef::Device(d) => match d.class() { - device_registry::DeviceType::Keyboard - | device_registry::DeviceType::PciFunction + device_registry::DeviceType::PciFunction | device_registry::DeviceType::Isa | device_registry::DeviceType::Acpi => false, - device_registry::DeviceType::Mouse - | device_registry::DeviceType::Framebuffer - | device_registry::DeviceType::Partition => true, + device_registry::DeviceType::Framebuffer => true, }, KObjectRef::PipeRead(_) | KObjectRef::PipeWrite(_) | KObjectRef::Connection(_) | KObjectRef::Acceptor(_) | KObjectRef::File(_) | KObjectRef::Inbox(_) @@ -411,37 +403,7 @@ pub fn read_device( buf: &mut UserBytesMut, ) -> Option { match claim.class() { - // Reads the queue only and drives no hardware: polling the controller here can block on its recovery engine. - // `drain_irqs` calls this same read at the top of every scheduler pass, so skipping the poll here still bounds staleness to one scheduler pass. - device_registry::DeviceType::Keyboard | device_registry::DeviceType::Mouse => { - match claim.class() { - device_registry::DeviceType::Keyboard => { - let event_size = core::mem::size_of::(); - let mut count = 0; - while count + event_size <= buf.len() { - let Some(event) = keyboard::try_read_event() else { break }; - buf.write_at(count, toyos_abi::usersafe::bytes(&event)); - count += event_size; - } - if count > 0 { Some(count as u64) } else { None } - } - device_registry::DeviceType::Mouse => { - let event_size = core::mem::size_of::(); - let mut count = 0; - while count + event_size <= buf.len() { - let Some(event) = mouse::try_read_event() else { break }; - buf.write_at(count, toyos_abi::usersafe::bytes(&event)); - count += event_size; - } - if count > 0 { Some(count as u64) } else { None } - } - other => panic!("a {other:?} claim answers with events"), - } - } device_registry::DeviceType::Framebuffer => Some(claim.describe(table, buf)), - // Every read is the description: a partition's bytes move through - // `SYS_PARTITION_READ`, never through a read of the claim. - device_registry::DeviceType::Partition => Some(claim.describe(table, buf)), // The description first and interrupts after: a driver reads what it // is driving once, and everything it reads afterwards is what its // device has been doing. @@ -674,125 +636,23 @@ pub fn fstat(object: &KObjectRef) -> Stat { | KObjectRef::Connector(_) | KObjectRef::Namespace(_) | KObjectRef::Process(_) => plain(FileType::Unknown), KObjectRef::Device(d) => plain(match d.class() { - device_registry::DeviceType::Keyboard => FileType::Keyboard, - device_registry::DeviceType::Mouse => FileType::Mouse, device_registry::DeviceType::Framebuffer => FileType::Framebuffer, device_registry::DeviceType::PciFunction | device_registry::DeviceType::Isa | device_registry::DeviceType::Acpi => FileType::Unknown, - device_registry::DeviceType::Partition => FileType::Unknown, }), } } -/// `SYS_FSYNC`: a partition claim's writes on its device and the device told to -/// commit them; a kernel file's pages are the file (`/tmp`) or never written -/// (ROOT), so it owes nothing. +/// `SYS_FSYNC`: a kernel file's pages are the file (`/tmp`) or never written +/// (ROOT), so it owes nothing; nothing else here holds writes. pub fn fsync(object: &KObjectRef) -> u64 { match object { KObjectRef::File(_) => 0, - KObjectRef::Device(claim) => partition_fsync(claim), _ => SyscallError::PermissionDenied.to_u64(), } } -/// What a run of [`until_answered`]'s attempts came to. -pub(crate) enum Answered { - /// An attempt answered with a word of the device's own: `Ok`, or a failure - /// it named. - Answer { answer: Result<(), SyscallError>, attempts: u32, took: crate::time::Duration }, - /// The caller is being killed; the word dies with its task. - Killed, - /// Every attempt until [`crate::block::DEADMAN`] was refused on its budget: - /// the caller answers `Io`, a device word, rather than another ask-again. - Deadman { attempts: u32, took: crate::time::Duration }, -} - -/// `attempt` run until it answers anything but `WouldBlock` — a budget that -/// expired on a live device, never a device fact — each time on a fresh -/// budget, parked between two (`block::between_attempts`), and given up once -/// [`crate::block::DEADMAN`] is spent. The one loop in this kernel that asks a -/// block device again, for a caller holding no spinlock: nothing it holds can -/// be held across the wait, so no disk wait here is under one. -pub(crate) fn until_answered(mut attempt: impl FnMut() -> Result<(), SyscallError>) -> Answered { - let began = crate::clock::now(); - // Bounds the run of attempts, never a single attempt's elapsed time. - let deadman = Deadline::at(began + crate::block::DEADMAN.duration()); - let mut attempts = 0u32; - loop { - attempts += 1; - let answer = attempt(); - if answer != Err(SyscallError::WouldBlock) { - return Answered::Answer { answer, attempts, took: crate::clock::now() - began }; - } - // A killed caller stops retrying at the first safe point. The machine's stop is deliberately not read here: `quiesce` claims every filesystem is synced, and a sync it named may not be abandoned by the stop that is about to make that claim. - if crate::sched::driver::current_kill_pending() { - return Answered::Killed; - } - if deadman.reached(crate::clock::now()) { - return Answered::Deadman { attempts, took: crate::clock::now() - began }; - } - crate::block::between_attempts(attempts); - } -} - -/// A block-device failure as the word a syscall returns: a budget that expired -/// is asked again ([`until_answered`]), anything else is the device's. -pub(crate) fn block_word(e: crate::block::BlockError) -> SyscallError { - match e { - crate::block::BlockError::Device => SyscallError::Io, - crate::block::BlockError::BudgetExpired => SyscallError::WouldBlock, - } -} - -/// `SYS_FSYNC` on a partition claim: every write the claim returned from -/// before this call durable, or the claim told they may not be. The flush is -/// the whole device's, and its answer is the claim's own -/// (`block::Partition::flush`). -fn partition_fsync(claim: &DeviceClaim) -> u64 { - match claim.class() { - device_registry::DeviceType::Partition => {} - device_registry::DeviceType::Keyboard - | device_registry::DeviceType::Mouse - | device_registry::DeviceType::Framebuffer - | device_registry::DeviceType::PciFunction - | device_registry::DeviceType::Isa - | device_registry::DeviceType::Acpi => { - return SyscallError::PermissionDenied.to_u64(); - } - } - let run = until_answered(|| match claim.partition_view() { - Some(view) => view.flush().map_err(block_word), - None => Err(SyscallError::Gone), - }); - if let (Answered::Answer { answer: Ok(()), attempts, took }, Some((_, guid))) = (&run, claim.partition_on()) { - if *attempts > 1 { - crate::log!( - "partclaim: a flush of {} durable on attempt {attempts} after {took} — a refused \ - attempt was asked again on a fresh budget", - toyos_gpt::Guid(guid), - ); - } - } - partition_word("a flush", run) -} - -/// What a partition claim's run of attempts at `what` answers its caller. -pub(crate) fn partition_word(what: &str, run: Answered) -> u64 { - match run { - Answered::Answer { answer: Ok(()), .. } => 0, - Answered::Answer { answer: Err(e), .. } => e.to_u64(), - Answered::Killed => SyscallError::WouldBlock.to_u64(), - Answered::Deadman { attempts, took } => { - crate::log!( - "partclaim: {what} still refused after {attempts} attempt(s) in {took} — {}", - crate::block::DEADMAN, - ); - SyscallError::Io.to_u64() - } - } -} - pub fn ftruncate(object: &KObjectRef, size: u64) -> u64 { let KObjectRef::File(file) = object else { return SyscallError::PermissionDenied.to_u64(); @@ -818,8 +678,6 @@ pub fn has_data(object: &KObjectRef) -> bool { KObjectRef::Process(p) => p.finished(), KObjectRef::File(_) | KObjectRef::Null(_) => true, KObjectRef::Device(d) => match d.class() { - device_registry::DeviceType::Keyboard => keyboard::has_data(), - device_registry::DeviceType::Mouse => mouse::has_data(), // A claim its last handle has let go has nothing to read: a poll // on it is ended where it registers (`DeviceClaim::add_poll`). device_registry::DeviceType::PciFunction => { @@ -829,7 +687,6 @@ pub fn has_data(object: &KObjectRef) -> bool { !d.info_read() || d.isa(crate::isa::has_irq).unwrap_or(false) } device_registry::DeviceType::Framebuffer => true, - device_registry::DeviceType::Partition => true, }, KObjectRef::PipeWrite(_) | KObjectRef::Inbox(_) | KObjectRef::SysCap(_) | KObjectRef::Connector(_) | KObjectRef::Namespace(_) @@ -839,13 +696,11 @@ pub fn has_data(object: &KObjectRef) -> bool { /// Whether a post on this object's read watch is its readability, which /// [`has_data`] cannot be asked for: the log's, whose unread records are a -/// property of the reader's cursor, which the kernel does not hold; and a -/// console's, whose watch is the keyboard's while its data is the serial -/// line's (`issues/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md`). +/// property of the reader's cursor, which the kernel does not hold. pub fn read_posts_are_readiness(object: &KObjectRef) -> bool { match object { - KObjectRef::SysCap(_) | KObjectRef::Console(_) => true, - KObjectRef::PipeRead(_) | KObjectRef::PipeWrite(_) | KObjectRef::Connection(_) + KObjectRef::SysCap(_) => true, + KObjectRef::Console(_) | KObjectRef::PipeRead(_) | KObjectRef::PipeWrite(_) | KObjectRef::Connection(_) | KObjectRef::Acceptor(_) | KObjectRef::File(_) | KObjectRef::Device(_) | KObjectRef::Inbox(_) | KObjectRef::Connector(_) | KObjectRef::Namespace(_) | KObjectRef::SharedMem(_) | KObjectRef::Process(_) | KObjectRef::Null(_) => false, @@ -890,11 +745,9 @@ pub fn mark_tty(object: &KObjectRef) -> u64 { fn write_device(claim: &DeviceClaim, buf: &UserBytes) -> u64 { match claim.class() { device_registry::DeviceType::Isa | device_registry::DeviceType::Acpi => {} - device_registry::DeviceType::Keyboard - | device_registry::DeviceType::Mouse - | device_registry::DeviceType::Framebuffer - | device_registry::DeviceType::PciFunction - | device_registry::DeviceType::Partition => return SyscallError::PermissionDenied.to_u64(), + device_registry::DeviceType::Framebuffer | device_registry::DeviceType::PciFunction => { + return SyscallError::PermissionDenied.to_u64(); + } } if !claim.info_read() { return SyscallError::PermissionDenied.to_u64(); diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index fda9fd42685..028d0be056f 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -155,6 +155,8 @@ const CONFIG_BYTES: u64 = 4096; /// drives, and what outlives a holder. struct Function { who: u16, + /// Its configuration space, which the reset's stop writes with no lock. + pci: PciDevice, /// Whether a claim holds it: taken by [`claim`] before the function is /// touched, and given back last by [`release`]. /// @@ -200,6 +202,20 @@ fn functions() -> &'static [Function] { unsafe { *published } } +/// Every function a claim holds, stopped before the machine resets or powers +/// off with its holder alive: Bus Master Enable cleared, which ends its DMA +/// and its messages, since a message is a write it masters (PCIe Base +/// §7.5.1.1); MSI Enable cleared (§7.7.1) and MSI-X's Function Mask set +/// (§7.7.2) as well. Registers alone, with no lock and no allocation: a panic +/// and a wedge reach it. +pub fn stop_before_reset() { + for function in functions().iter().filter(|f| f.held.load(Ordering::Acquire)) { + function.pci.disable_bus_master(); + function.pci.disable_msi(); + function.pci.mask_msix_function(); + } +} + /// The arrival of function `number`, counted from 1 so a zeroed record names /// none. fn arrival_of(number: usize) -> Arrival { @@ -468,11 +484,6 @@ pub fn with_bar_memory(f: impl FnOnce(&mut dyn Iterator) - })) } -/// The PCI segment group every enumerated function is on. -pub fn segment() -> u16 { - MACHINE.lock().segment -} - /// The function a claim holds. pub fn held_at(binding: &Binding) -> toyos_abi::inventory::PciAddr { addr_of(MACHINE.lock().segment, binding.function().who) @@ -551,6 +562,7 @@ pub fn publish(devices: &[PciDevice], segment: u16, firmware: &[RootBridgeWindow .iter() .map(|device| Function { who: requester(device), + pci: *device, held: AtomicBool::new(false), irq: Interrupt::new(), watch: IrqWatch::new(), diff --git a/kernel/src/power.rs b/kernel/src/power.rs index 77ad74d493f..54ff58f0da0 100644 --- a/kernel/src/power.rs +++ b/kernel/src/power.rs @@ -1,13 +1,14 @@ //! The machine's two ends this kernel performs, a reset and a power-off, each //! the architecture's own (`arch::power`). //! -//! **No reset this kernel performs leaves a USB device mid-command.** +//! **No reset this kernel performs leaves a claimed function mastering.** //! [`reset_now`] and [`shutdown`] are the only two places this kernel ends the -//! machine, and each stops every xHCI controller ([`stop::before_reset`]) -//! before it does — which is what makes that a property of the reset rather -//! than of whoever asked for one, and what a third caller gets without knowing -//! it is owed. Resets this kernel does not perform — a TCO or firmware -//! watchdog, a triple fault, power loss — are outside it and always will be. +//! machine, and each stops every function a claim holds +//! ([`crate::pcidev::stop_before_reset`]) before it does — which is what +//! makes that a property of the reset rather than of whoever asked for one, +//! and what a third caller gets without knowing it is owed. Resets this +//! kernel does not perform — a TCO or firmware watchdog, a triple fault, +//! power loss — are outside it and always will be. //! //! **Nothing an end says is logged after the console's last drain.** The //! stop holds the console's wire (`log::console::StopWire`), or writes over @@ -19,7 +20,6 @@ //! earlier one: its tail is sealed before either end is entered //! (`log::seal_tail`), so what `settle` says is on the console and in no page. -use crate::drivers::xhci::stop; use crate::log::console::StopWire; /// Whether this machine has a reset this kernel can perform. @@ -51,15 +51,10 @@ pub fn reboot(mut wire: StopWire) -> ! { /// A machine with no reset halts here rather than returning: the caller has /// already sealed why, and holding is what such a machine has always done. pub fn reset_now() -> ! { - // **Here and not at either caller.** `stop::before_reset` is registers and - // nothing else — written for the panic path, so it takes no lock and - // allocates nothing, which is the only kind of call a wedge may make — and - // it *appends* its account to whatever this boot already sealed, so a - // `WEDGED` page carries what the reset did to USB the way a `PANIC` one - // does. A caller seals first and calls this second: the record is the - // diagnostic the seal exists for and may not be lost to a stop that does - // not return. - stop::before_reset(); + // **Here and not at either caller**: the stop is registers and nothing + // else, written for the panic path, so it takes no lock and allocates + // nothing, which is the only kind of call a wedge may make. + crate::pcidev::stop_before_reset(); crate::arch::power::reset() } @@ -69,9 +64,6 @@ pub fn shutdown(stopping: crate::quiesce::Stopping, mut wire: StopWire) -> ! { let settled = crate::arch::power::settle(stopping); // Nothing drains the log ring after this point, and nothing below logs. wire.drain(); - // A power-off takes VBUS with it on a machine whose ports are not - // always-on and takes nothing on one whose are, so the devices are handed - // back here for the same reason as at a reboot. - stop::before_reset(); + crate::pcidev::stop_before_reset(); crate::arch::power::off(settled) } diff --git a/kernel/src/quiesce.rs b/kernel/src/quiesce.rs index a82b0a6275a..30983483524 100644 --- a/kernel/src/quiesce.rs +++ b/kernel/src/quiesce.rs @@ -51,12 +51,11 @@ use crate::time::{Budget, Deadline, Duration}; /// How long the machine gets to stop. /// /// **A budget, and not a bound the kernel can prove.** One `QUANTUM_NS` is -/// what a thread running in Ring 3 needs to reach the boundary, and one -/// `block::OPERATION` is the longest a thread lies inside the block layer -/// without parking. A thread can therefore outlast this, which is why its -/// expiry is a clause in the record. +/// what a thread running in Ring 3 needs to reach the boundary, and as long +/// again covers one inside a syscall that does not park. A thread can +/// therefore outlast this, which is why its expiry is a clause in the record. pub(crate) const PARK: Budget = Budget::of( - Duration::from_nanos(kernel::sched::fair::QUANTUM_NS + crate::block::OPERATION.nanos()), + Duration::from_nanos(2 * kernel::sched::fair::QUANTUM_NS), "the reset lands wherever the threads that never reached a safe point are, and \ the record names how many", ); @@ -179,7 +178,6 @@ pub fn stop() -> (Record, Stopping) { // Read here and not by the caller: the question is what was open at the // moment the stop ended, and every line between here and the record's // own would open more. - let (in_flight, begun) = crate::block::userland_operations(); return ( Record { sweep: swept, @@ -187,8 +185,6 @@ pub fn stop() -> (Record, Stopping) { budget_ms: budget.nanos() / 1_000_000, sweeps, cpus, - in_flight, - begun, }, Stopping(()), ); diff --git a/kernel/src/rootfs.rs b/kernel/src/rootfs.rs index 926cbd5e04e..28c1bce039f 100644 --- a/kernel/src/rootfs.rs +++ b/kernel/src/rootfs.rs @@ -7,9 +7,7 @@ //! inside one `LoaderData` descriptor of firmware's map, returning the region //! `mm::init` keeps out of the allocator. [`mount`] refuses the boot by name on //! a handoff with no image, and on an image whose superblock is not the one -//! `root=` names. [`hold_source`] holds the partition the image came from once -//! the disks are up, so no claim writes the slot this boot runs, and withholds -//! its GUID from every claim when it cannot hold it. The image's bytes crossed a +//! `root=` names. The image's bytes crossed a //! trust boundary like any disk's, so every read of it is bounds-checked and a //! block outside it is a refused read, never a panic. @@ -17,18 +15,13 @@ use bcachefs::{BlockBuf, BlockIO, BlockNum, DeviceError, FsUuid, Mounted, ReadOn use toyos_abi::boot::{KernelArgs, MemoryMapEntry}; use toyos_rootimage::handoff::{held, Descriptor}; -use crate::block::{BlockError, Holder, Partition}; -use crate::gpt::Unnamed; +use crate::file_backing::BlockError; use crate::mm::{DirectMap, Region}; use crate::sync::Lock; /// The unit ROOT's filesystem is written in, which is the page. const BLOCK: usize = crate::mm::PAGE_SIZE as usize; -/// The record the supervisor's spawn is reported on, followed by how many storage -/// commands the boot had issued by then: zero is the claim. -pub const SUPERVISOR_WITHOUT_A_DISK: &str = "boot: the supervisor spawned with ROOT from memory; storage commands before it:"; - /// The record a ROOT mounted off the loader's image is reported on. pub const MOUNTED_FROM_MEMORY: &str = "root: mounted read-only from memory at"; @@ -47,14 +40,9 @@ enum Handed { struct Boot { named: Option, handed: Handed, - /// The partition the image was read from, raw as in its GPT entry. - source: [u8; 16], } -static BOOT: Lock = Lock::new(Boot { named: None, handed: Handed::Nothing, source: [0; 16] }); - -/// The kernel's hold on the partition ROOT was read from, for the machine's life. -static SOURCE: Lock> = Lock::new(None); +static BOOT: Lock = Lock::new(Boot { named: None, handed: Handed::Nothing }); /// ROOT's bytes: whole blocks, read-only, in memory the kernel never frees. #[derive(Clone, Copy)] @@ -118,7 +106,6 @@ pub fn init(cmdline: &str, args: &KernelArgs, map: &[MemoryMapEntry]) -> Region *BOOT.lock() = Boot { named: toyos_abi::boot::root_uuid(cmdline).and_then(FsUuid::parse), handed, - source: args.root_partition_guid, }; region } @@ -149,60 +136,3 @@ pub fn mount() -> Mounted { log!("{MOUNTED_FROM_MEMORY} {at:#x}+{len:#x}, filesystem {named}, {} blocks", image.block_count()); fs } - -/// Hold the partition ROOT was read from, so no process's claim writes the -/// slot this boot is running. Runs once the disks are probed. -/// -/// Found on the disks that answered, its span is held: a disk that did not -/// answer and later does either lacks it, or carries it again and makes every -/// claim of it `Ambiguous`. Anywhere else — on no disk that answered, carried -/// twice, refused by its own table, or no span a view can hold — its GUID is -/// withheld from every claim instead, so no disk's answer, now or later, can -/// hand it out. Neither path refuses the boot: a disk is a device, and a device -/// never crashes this kernel. -pub fn hold_source() { - let guid = toyos_abi::part::PartGuid(BOOT.lock().source); - let sought = crate::gpt::seek(guid); - let held: Result<(crate::gpt::Claimable, Partition), &'static str> = match sought.found { - Ok(Some(found)) => { - let volume = found.volume; - crate::block::open(volume.device) - .ok_or(()) - .and_then(|handle| { - let (first, blocks) = - crate::block::span_blocks(volume.start_lba, volume.blocks, volume.lba_bytes) - .map_err(drop)?; - Partition::of(handle, first, blocks, Holder::Kernel("system")).map_err(drop) - }) - .map(|view| (found, view)) - .map_err(|()| "it is no span a view can hold") - } - // Every disk this kernel drives answered and lacks it: a disk a process - // drives, NVMe's under diskserver, is never asked. - Ok(None) if sought.silent.is_empty() => Err("it is on no disk this kernel drives"), - Ok(None) => Err("it is on no disk that answered"), - Err(Unnamed::Ambiguous) => Err("it is carried twice"), - Err(Unnamed::Unusable) => Err("its table refuses it"), - }; - match held { - Ok((found, view)) => { - log!( - "root: holding {}, the partition ROOT was read from, on device {}; disks that did \ - not answer: {:?}", - found.unique, found.volume.device, sought.silent - ); - *SOURCE.lock() = Some(view); - } - Err(why) => withhold(guid, why, &sought.silent), - } -} - -/// Refuse every claim of ROOT's source, saying why it was not held. -fn withhold(guid: toyos_abi::part::PartGuid, why: &str, silent: &[crate::block::DeviceId]) { - log!( - "root: the partition ROOT was read from, {}, is not held because {why}; every claim of it \ - is refused; disks that did not answer: {silent:?}", - toyos_gpt::Guid(guid.0) - ); - crate::gpt::withhold(guid); -} diff --git a/kernel/src/sched/driver.rs b/kernel/src/sched/driver.rs index ba378b5353a..2da327c1f55 100644 --- a/kernel/src/sched/driver.rs +++ b/kernel/src/sched/driver.rs @@ -518,11 +518,10 @@ pub fn pass(dispose: Dispose) { crate::preempt::disable(); #[cfg(feature = "boot-actuators")] crate::deadline::wedge_if_staged(); - // Must clear before it drains, so a wake from this pass's own drain survives into the next poll. + // Cleared before the pass, so a wake landing during it survives into the next poll. crate::preempt::clear_need_resched(); - drain_irqs(); - // After `drain_irqs` and before the pass picks, so a wake this - // posts is in the run queue by the time the pass chooses. + // Before the pass picks, so a wake this posts is in the run queue by the + // time the pass chooses. crate::object::drain_zero_handles(); let now = HW.now(); crate::arch::watchdog::feed(now.0); @@ -602,7 +601,6 @@ pub fn pass_block(ticket: Ticket, deadline: Option) { // No `preempt::disable()` of its own: the ticket has held the count raised since registration; that guard is this bracket. let ticket = ticket.into_raw(); crate::preempt::clear_need_resched(); - drain_irqs(); let now = HW.now(); let (action, parked) = with_cpu(|cpu| { let pass = SchedPass::begin(cpu, env(&PreemptOff(())), now); @@ -656,10 +654,8 @@ fn execute(action: Action) { let cpu = CpuId(percpu::cpu_id()); let awake = cpus().get(cpu).doorbell().kick_pending() || crate::preempt::need_resched() - || !with_cpu(|c| c.mailbox_is_empty()) - // 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(); + // No log condition here: a log to write means a runnable process, covered above. + || !with_cpu(|c| c.mailbox_is_empty()); if awake { crate::arch::cpu::enable_interrupts(); drop(token); @@ -670,11 +666,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(); -} - /// Leave the current stack for this CPU's idle stack and never come back. pub fn enter_idle_loop() -> ! { #[cfg(feature = "mask-windows")] @@ -721,11 +712,6 @@ pub fn current_handle() -> Option> { try_with_cpu(|cpu| cpu.running().map(|t| t.ext().handle.clone())).flatten() } -/// The same face, borrowed rather than cloned, for a peek that does not outlive it — [`current_handle`]'s clone is a hot-path RMW. -pub fn with_current_handle(f: impl FnOnce(&crate::sched::payload::TaskHandle) -> R) -> Option { - try_with_cpu(|cpu| cpu.running().map(|t| f(t.ext().handle.as_ref())))? -} - /// The image of the task this CPU is running, `None` inside for a kernel thread. /// /// Takes no lock: the image is immutable and the `Arc` outlives teardown. `None`: a pass already holds this diff --git a/kernel/src/sched/kthread.rs b/kernel/src/sched/kthread.rs index 55a7119020a..9758e78c1fa 100644 --- a/kernel/src/sched/kthread.rs +++ b/kernel/src/sched/kthread.rs @@ -65,8 +65,8 @@ pub fn current_is_kernel_thread() -> bool { } /// Is `id` a kernel thread? -// No lock: `drain_irqs` calls this on a machine already suspected of being -// stuck, where taking one could hang diagnostics. +// No lock: the stop asks this on a machine already suspected of being stuck, +// where taking one could hang diagnostics. pub fn is_kernel_task(id: TaskId) -> bool { let packed = id.pack(); ROWS.iter().any(|row| row.load(Ordering::Relaxed) == packed) diff --git a/kernel/src/sched/payload.rs b/kernel/src/sched/payload.rs index e60775e9bda..398f0e29cfe 100644 --- a/kernel/src/sched/payload.rs +++ b/kernel/src/sched/payload.rs @@ -15,7 +15,6 @@ use kernel::sched::park::WaitTicket; use crate::watch::Watch; use crate::mm::paging::Root; -use crate::scheduler::OperationSlot; use crate::loader::KernelStack; use crate::process::{PageTables, ProcessAccounting, TaskId, UserImage}; use crate::sync::Lock; @@ -112,8 +111,6 @@ pub struct TaskHandle { watch: Watch, /// Cancels reported to this thread; a second one means a caller swallowed the first, so it panics rather than spinning. cancels: AtomicU32, - /// The operation this thread is inside, if any; kept here so it survives a mid-operation migration. `scheduler::Operation` owns the rules. - operation: OperationSlot, /// The releases this thread owns while its process is torn down (`object::Releases`); kept here, as `operation` is, so they follow the thread across a migration. releases: Lock>>, } @@ -125,7 +122,6 @@ impl TaskHandle { running_since: AtomicU64::new(0), watch: Watch::new(), cancels: AtomicU32::new(0), - operation: OperationSlot::new(), releases: Lock::new(None), } } @@ -142,10 +138,6 @@ impl TaskHandle { self.running_since.store(0, Ordering::Relaxed); } - /// Where this thread's establishment lives; `scheduler::Operation` owns every rule about it. - pub fn operation(&self) -> &OperationSlot { - &self.operation - } /// Where `object::Releases` collects this thread's releases; `None` while it holds none open. pub(crate) fn releases(&self) -> &Lock>> { diff --git a/kernel/src/sched_gate.rs b/kernel/src/sched_gate.rs deleted file mode 100644 index b8b4e96cba3..00000000000 --- a/kernel/src/sched_gate.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! In-guest gate on [`Operation`] nesting: `begin` narrows the current -//! deadline and never widens it, and `Drop` restores what it displaced. -//! [`run`] is called from a boot-phase CPU context and from a syscall, the two -//! places a deadline slot can be established. - -use crate::clock; -use crate::scheduler::Operation; -use crate::time::{Deadline, Duration}; - -// INNER < OUTER < WIDER: level 2 must narrow, level 3 must change nothing. -const OUTER: u64 = 1_000_000_000; -const INNER: u64 = 250_000_000; -const WIDER: u64 = 4_000_000_000; - -/// Establishes three nested operations at `site` and logs what each level observed. -pub fn run(site: &str) { - let base = clock::now(); - log!( - "sched-op: {site} outside established={}", - Operation::established(), - ); - - let level = |until: u64| Deadline::at(base + Duration::from_nanos(until)); - let observed = || Operation::deadline().nanos() - base.nanos_since_boot(); - - let outer = Operation::begin(level(OUTER)); - log!( - "sched-op: {site} begin level=1 asked={OUTER} observed={}", - observed(), - ); - { - let inner = Operation::begin(level(INNER)); - log!( - "sched-op: {site} begin level=2 asked={INNER} observed={}", - observed(), - ); - { - // Asking for more than the enclosing frame must not widen it. - let _wider = Operation::begin(level(WIDER)); - log!( - "sched-op: {site} begin level=3 asked={WIDER} observed={}", - observed(), - ); - } - log!( - "sched-op: {site} end level=3 observed={} established={}", - observed(), - Operation::established(), - ); - drop(inner); - log!( - "sched-op: {site} end level=2 observed={} established={}", - observed(), - Operation::established(), - ); - } - drop(outer); - log!( - "sched-op: {site} end level=1 established={}", - Operation::established(), - ); -} diff --git a/kernel/src/scheduler.rs b/kernel/src/scheduler.rs index 4102dbf5065..d76e7a61cf7 100644 --- a/kernel/src/scheduler.rs +++ b/kernel/src/scheduler.rs @@ -1,11 +1,10 @@ //! The kernel-facing scheduler API surface: no decision, state transition or //! ordering-sensitive step happens here. //! -//! Exception: [`Parkable`] and [`Operation`] live here because the park token -//! has no public constructor outside the two doors this module defines. +//! Exception: [`Parkable`] lives here because the park token has no public +//! constructor outside the door this module defines. use alloc::sync::Arc; -use core::sync::atomic::{AtomicU64, Ordering}; use crate::hasher::HashMap; use kernel::sched::fair::{ShareState, QUANTUM_NS}; @@ -18,7 +17,7 @@ use crate::hw::HW; use crate::pipe::PipeId; use crate::process::{self, Pid, Tid}; use crate::sched::driver::{self, cpus, preempt_off, Dispose, NewTask}; -use crate::sched::payload::{KShare, KernelLock, TaskHandle, ThreadSched}; +use crate::sched::payload::{KShare, KernelLock, ThreadSched}; use crate::sched::reap_gate::ReapGate; use crate::sched::futex; use crate::sync::Lock; @@ -60,8 +59,7 @@ fn blocking_baseline() -> u32 { } /// Proof that the calling context may park; threaded by reference rather -/// than stored, with no public constructor beside [`Parkable::at_entry`] and -/// [`Operation::parkable`]. +/// than stored, with no public constructor beside [`Parkable::at_entry`]. pub struct Parkable(()); impl Parkable { @@ -69,11 +67,6 @@ impl Parkable { /// nothing below one, then mints the proof. #[track_caller] pub fn at_entry() -> Parkable { - assert!( - !Operation::established(), - "scheduler: a frame inside an established operation minted its own park \ - permission — a leaf receives one from the operation, it does not make one", - ); Parkable::mint() } @@ -84,138 +77,6 @@ impl Parkable { } } -/// One operation the running context is inside. Establishments nest; an -/// inner one may only narrow the deadline, and the guard restores what it -/// displaced on drop. -#[must_use = "an operation lasts exactly as long as this guard"] -pub struct Operation { - /// Held rather than re-derived, so the drop restores the slot even if - /// the task has migrated; `None` selects the per-CPU slot named by `cpu`. - task: Option>, - cpu: usize, - /// What the slot held before this establishment; `None` is "no operation". - outer: Option, -} - -/// Two words rather than one sentinel: [`Deadline`] is total over its range -/// and has no value left to mean "none". Read as a pair only by the writer -/// that wrote them, so Relaxed ordering between the two is sound. -pub struct OperationSlot { - live: core::sync::atomic::AtomicBool, - until: AtomicU64, -} - -impl OperationSlot { - pub const fn new() -> Self { - Self { - live: core::sync::atomic::AtomicBool::new(false), - until: AtomicU64::new(0), - } - } -} - -impl Default for OperationSlot { - fn default() -> Self { - Self::new() - } -} - -/// Where a context with no task establishes: one per CPU, since boot and an -/// idle CPU's pass cannot be moved off theirs. -fn no_task_operation(cpu: usize) -> &'static OperationSlot { - &crate::cpu_local::of(cpu as u32).no_task_operation -} - -impl Operation { - /// Declare the running context inside one operation, bounded by `until` - /// or by whatever already bounds it, whichever comes first. - pub fn begin(until: Deadline) -> Operation { - let task = driver::current_handle(); - let cpu = percpu::cpu_id() as usize; - let outer = { - let slot = operation_slot(&task, cpu); - let outer = slot - .live - .load(Ordering::Relaxed) - .then(|| slot.until.load(Ordering::Relaxed)); - slot.until.store( - outer.map_or(until.nanos(), |outer| outer.min(until.nanos())), - Ordering::Relaxed, - ); - slot.live.store(true, Ordering::Relaxed); - outer - }; - Operation { task, cpu, outer } - } - - /// The deadline the operation this depth is part of has left to spend. - /// Panics if no operation is established above this depth. - #[track_caller] - pub fn deadline() -> Deadline { - let (live, until) = Self::read(); - assert!( - live, - "scheduler: a depth asked for its operation's deadline with no operation \ - established above it", - ); - Deadline::at(crate::time::Instant::from_nanos_since_boot(until)) - } - - /// The park token of the operation this depth is part of. Panics if no - /// operation is established above this depth. No caller yet: - /// `xhci::wait_transfer` wants this, but the ticket locks above it fail - /// [`Parkable::mint`]'s baseline assertion until they convert. - #[allow(dead_code)] - #[track_caller] - pub fn parkable() -> Parkable { - assert!( - Self::established(), - "scheduler: a depth asked to park with no operation established above it", - ); - Parkable::mint() - } - - /// Whether the running context is inside one. - pub fn established() -> bool { - Self::read().0 - } - - /// A borrow and not a clone: `Arc::clone`'s read-modify-write is too - /// costly on this hot path. - fn read() -> (bool, u64) { - fn of(slot: &OperationSlot) -> (bool, u64) { - ( - slot.live.load(Ordering::Relaxed), - slot.until.load(Ordering::Relaxed), - ) - } - driver::with_current_handle(|task| of(task.operation())) - .unwrap_or_else(|| of(no_task_operation(percpu::cpu_id() as usize))) - } - - fn slot(&self) -> &OperationSlot { - operation_slot(&self.task, self.cpu) - } -} - -/// The slot a context establishes in: its task's, or its CPU's if it has none. -fn operation_slot(task: &Option>, cpu: usize) -> &OperationSlot { - match task { - Some(task) => task.operation(), - None => no_task_operation(cpu), - } -} - -impl Drop for Operation { - fn drop(&mut self) { - let slot = self.slot(); - match self.outer { - Some(until) => slot.until.store(until, Ordering::Relaxed), - None => slot.live.store(false, Ordering::Relaxed), - } - } -} - /// Process-scoped thread identity: tids are per-process only. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct TaskId(pub Pid, pub Tid); @@ -312,9 +173,7 @@ pub fn enqueue_new( /// Phase 1 of the wait handshake on the running thread's own word, or /// [`Refused`]: a post reached it since it registered on a watch, and the /// caller re-reads its condition, or a revoke ended the registration, and the -/// wait is over. Mints via [`Parkable::mint`], not [`Parkable::at_entry`]: a -/// context already inside an [`Operation`] must receive the token here too, -/// which `at_entry` would refuse. +/// wait is over. #[must_use = "a wait ticket must be blocked on or cancelled"] #[track_caller] pub fn prepare_wait(cancel: Cancel, class: WaitClass) -> Result { diff --git a/kernel/src/sync.rs b/kernel/src/sync.rs index 24773c0d81c..58d15168845 100644 --- a/kernel/src/sync.rs +++ b/kernel/src/sync.rs @@ -1,15 +1,14 @@ -use core::mem::ManuallyDrop; use core::ops::{Deref, DerefMut}; #[cfg(not(feature = "loom"))] use core::cell::UnsafeCell; #[cfg(not(feature = "loom"))] -use core::sync::atomic::{AtomicU32, AtomicU64, Ordering}; +use core::sync::atomic::{AtomicU32, Ordering}; #[cfg(feature = "loom")] use crate::cell::UnsafeCell; #[cfg(feature = "loom")] -use loom::sync::atomic::{AtomicU32, AtomicU64, Ordering}; +use loom::sync::atomic::{AtomicU32, Ordering}; // Unlock publishes through `now`, not `ticket`; the load that reads `now` // is the one that must carry the acquire. @@ -230,20 +229,6 @@ impl Drop for LockGuard<'_, T> { } } -/// The pair an owing CPU's retry and a release's look each put before their -/// read, so at least one of them sees the other's write. `owed-fence-off` -/// drops it so `kernel-loom`'s `owed_lock` model can prove it is load-bearing. -#[cfg(not(feature = "owed-fence-off"))] -fn owed_fence() { - #[cfg(not(feature = "loom"))] - use core::sync::atomic::fence; - #[cfg(feature = "loom")] - use loom::sync::atomic::fence; - fence(Ordering::SeqCst); -} -#[cfg(feature = "owed-fence-off")] -fn owed_fence() {} - /// A lock taken only through a borrow of a closed `IrqGuard`: never before /// the mask, and never held past it, so an interrupt handler may take it too. #[cfg(not(feature = "loom"))] @@ -259,81 +244,3 @@ impl Masked { self.0.lock_masked(closed) } } - -/// A [`Lock`] a CPU may find taken and leave without waiting: a CPU that -/// [`OwedLock::try_lock_or_owe`] turned away is owed, and the next release hands -/// every owed CPU, one bit each, to the `answer` the lock was built with, which -/// brings it back to try again. What it owes is the turned-away CPU's to know. -pub struct OwedLock { - lock: Lock, - owed: AtomicU64, - answer: fn(u64), -} - -impl OwedLock { - #[cfg(not(feature = "loom"))] - pub const fn new(val: T, answer: fn(u64)) -> Self { - Self { lock: Lock::new(val), owed: AtomicU64::new(0), answer } - } - - #[cfg(feature = "loom")] - pub fn new(val: T, answer: fn(u64)) -> Self { - Self { lock: Lock::new(val), owed: AtomicU64::new(0), answer } - } - - #[track_caller] - pub fn lock(&self) -> OwedGuard<'_, T> { - OwedGuard { guard: ManuallyDrop::new(self.lock.lock()), of: self } - } - - pub fn try_lock(&self) -> Option> { - Some(OwedGuard { guard: ManuallyDrop::new(self.lock.try_lock()?), of: self }) - } - - /// [`Self::try_lock`], or `cpu` is owed: `None` only when the holder's - /// release is still to look, and that look answers `cpu`. - pub fn try_lock_or_owe(&self, cpu: u32) -> Option> { - if let Some(won) = self.try_lock() { - return Some(won); - } - let bit = 1u64.checked_shl(cpu).expect("OwedLock: a cpu id past its 64-bit owed word"); - self.owed.fetch_or(bit, Ordering::Relaxed); - owed_fence(); - let won = self.try_lock()?; - self.owed.fetch_and(!bit, Ordering::Relaxed); - Some(won) - } -} - -pub struct OwedGuard<'a, T> { - guard: ManuallyDrop>, - of: &'a OwedLock, -} - -impl Deref for OwedGuard<'_, T> { - type Target = T; - fn deref(&self) -> &T { - &self.guard - } -} - -impl DerefMut for OwedGuard<'_, T> { - fn deref_mut(&mut self) -> &mut T { - &mut self.guard - } -} - -impl Drop for OwedGuard<'_, T> { - fn drop(&mut self) { - // Held across the release and the look, so no switch stands between an owed CPU and its answer. - crate::preempt::disable(); - // SAFETY: `guard` is dropped here, once, and nothing reads it after. - unsafe { ManuallyDrop::drop(&mut self.guard) }; - owed_fence(); - let owed = self.of.owed.swap(0, Ordering::Relaxed); - if owed != 0 { - (self.of.answer)(owed); - } - crate::preempt::enable(); - } -} diff --git a/kernel/src/syscall/device.rs b/kernel/src/syscall/device.rs index 99b144cab9f..94b0e957fbb 100644 --- a/kernel/src/syscall/device.rs +++ b/kernel/src/syscall/device.rs @@ -104,7 +104,7 @@ pub(super) fn sys_acpi(ctx: &SyscallContext, handle: RawHandle, op: u64, at: u64 /// Mints a device claim, gated on a `SysCap` carrying [`Rights::DEVICE`]. /// /// `selector` says which device where the class alone does not — a PCI -/// function's arrival, a partition's GUID — and a word the class +/// function's arrival, an ISA set — and a word the class /// does not read is refused, never dropped: whoever set it meant a device this /// claim would not name. pub(super) fn sys_device_claim(syscap: RawHandle, class: u64, selector: [u64; 2]) -> u64 { @@ -112,12 +112,9 @@ pub(super) fn sys_device_claim(syscap: RawHandle, class: u64, selector: [u64; 2] return SyscallError::InvalidArgument.to_u64(); }; let read = match class { - device::DeviceType::Keyboard - | device::DeviceType::Mouse - | device::DeviceType::Framebuffer - | device::DeviceType::Acpi => 0, + device::DeviceType::Framebuffer | device::DeviceType::Acpi => 0, device::DeviceType::PciFunction => 1, - device::DeviceType::Partition | device::DeviceType::Isa => 2, + device::DeviceType::Isa => 2, }; if selector[read..].iter().any(|&word| word != 0) { return SyscallError::InvalidArgument.to_u64(); @@ -131,7 +128,6 @@ pub(super) fn sys_device_claim(syscap: RawHandle, class: u64, selector: [u64; 2] Ok(c) => c, Err(device::ClaimError::Absent) => return SyscallError::NotFound.to_u64(), Err(device::ClaimError::Owned) => return SyscallError::AlreadyExists.to_u64(), - Err(device::ClaimError::Ambiguous) => return SyscallError::InvalidArgument.to_u64(), Err(device::ClaimError::KernelDriven) => { return SyscallError::PermissionDenied.to_u64() } @@ -341,82 +337,3 @@ fn grant_bytes(grant: &toyos_abi::pci::DmaGrant) -> &[u8] { ) } } - -/// Which way one partition transfer moves bytes, with the caller's window. -pub(super) enum Transfer<'a, 'u> { - Read(&'a mut crate::user_ptr::UserBytesMut<'u>), - Write(&'a crate::user_ptr::UserBytes<'u>), -} - -/// `count` blocks of a claimed partition from its block `first`, through a -/// kernel buffer: the device never sees a user address. -/// -/// The view is taken from the claim for each attempt and dropped before the -/// wait between two: while an operation is on the device its view holds the -/// partition, so a close on another thread cannot hand the partition to a -/// second holder under a write in flight, and no view is left on a stack a -/// kill could strand. A run that does not end inside the partition is refused -/// here without a word to the log — a caller can ask at syscall rate — and -/// `block::Partition::locate` stands behind this as the one check between a -/// block number and the device. -pub(super) fn sys_partition_transfer( - handle: RawHandle, - first: u64, - count: u32, - transfer: Transfer<'_, '_>, -) -> u64 { - let right = match transfer { - Transfer::Read(_) => Rights::READ, - Transfer::Write(_) => Rights::WRITE, - }; - let claim = match process::with_process_data(|data| { - data.handles.get::(handle, right) - }) { - Ok(claim) => claim, - Err(e) => return e.refuse(), - }; - let class = claim.class(); - match class { - device::DeviceType::Partition => {} - device::DeviceType::Keyboard - | device::DeviceType::Mouse - | device::DeviceType::Framebuffer - | device::DeviceType::PciFunction - | device::DeviceType::Isa - | device::DeviceType::Acpi => { - drop(claim); - return crate::object::HandleError::WrongType { - held: class.class_name(), - wanted: "a partition claim", - } - .refuse(); - } - } - match claim.partition_view().map(|view| view.fits(first, count)) { - None => return SyscallError::Gone.to_u64(), - Some(false) => return SyscallError::InvalidArgument.to_u64(), - Some(true) => {} - } - let mut bounce = alloc::vec![0u8; count as usize * toyos_abi::part::BLOCK_BYTES]; - match transfer { - Transfer::Write(from) => { - from.read_at(0, &mut bounce); - let run = ops::until_answered(|| match claim.partition_view() { - Some(view) => view.write_blocks(first, count, &bounce).map_err(ops::block_word), - None => Err(SyscallError::Gone), - }); - ops::partition_word("a write", run) - } - Transfer::Read(into) => { - let run = ops::until_answered(|| match claim.partition_view() { - Some(view) => view.read_blocks(first, count, &mut bounce).map_err(ops::block_word), - None => Err(SyscallError::Gone), - }); - let word = ops::partition_word("a read", run); - if word == 0 { - into.write_at(0, &bounce); - } - word - } - } -} diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index 0e0b5dd4c0c..2f715976516 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -24,7 +24,7 @@ use super::debug::{canary, debug_heap_alloc, ring0_timer_in_syscall, FATAL_HALT_ use super::device::{ sys_acpi, sys_device_bar_map, sys_device_claim, sys_device_dma_alloc, sys_device_dma_map, sys_device_dma_limit, sys_device_dma_start, sys_device_dma_unmap, - sys_device_reg_read, sys_partition_transfer, Transfer, + sys_device_reg_read, }; use super::fs::{ spawn_cwd, sys_chdir, sys_delete, sys_getcwd, sys_mkdir, sys_open, sys_readdir, sys_readlink, @@ -52,24 +52,16 @@ use super::proc::{ }; use super::vm::{shared_image, sys_dlopen, sys_dlsym, sys_mmap, sys_munmap, sys_query_modules, sys_tls_alloc_block}; -/// `sched-operation-nesting`'s task half and `sysret-ss-probe`, run once a boot -/// on the first syscall: a task's own deadline slot, and a park that switches -/// away from it and back. +/// `sysret-ss-probe`, run once a boot on the first syscall: a park that +/// switches away from it and back. #[cfg(feature = "boot-actuators")] fn task_probes() { use core::sync::atomic::{AtomicBool, Ordering::Relaxed}; static RAN: AtomicBool = AtomicBool::new(false); - let nesting = crate::actuator::sched_operation_nesting(); - let ss = crate::actuator::sysret_ss_probe(); - if !(nesting || ss) || RAN.swap(true, Relaxed) { + if !crate::actuator::sysret_ss_probe() || RAN.swap(true, Relaxed) { return; } - if nesting { - crate::sched_gate::run("syscall"); - } - if ss { - crate::arch::hw::sysret_ss_probe(&crate::scheduler::Parkable::at_entry()); - } + crate::arch::hw::sysret_ss_probe(&crate::scheduler::Parkable::at_entry()); } pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> u64 { @@ -407,25 +399,6 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> sys_endowments(&mut buf) } SYS_DEVICE_CLAIM => sys_device_claim(RawHandle(a1 as u32), a2, [a3, a4]), - // The count is bounded before it becomes a window, and a transfer of - // nothing is refused rather than sent to a device as a zero-length command. - SYS_PARTITION_READ | SYS_PARTITION_WRITE => { - let count = a4; - if count == 0 || count > toyos_abi::part::MAX_BLOCKS_PER_CALL as u64 { - return SyscallError::InvalidArgument.to_u64(); - } - let len = count * toyos_abi::part::BLOCK_BYTES as u64; - let handle = RawHandle(a1 as u32); - if num == SYS_PARTITION_READ { - let Some(mut into) = ctx.user_bytes_mut(UserAddr::new(a3), len) else { - return bad_addr; - }; - sys_partition_transfer(handle, a2, count as u32, Transfer::Read(&mut into)) - } else { - let Some(from) = ctx.user_bytes(UserAddr::new(a3), len) else { return bad_addr }; - sys_partition_transfer(handle, a2, count as u32, Transfer::Write(&from)) - } - } SYS_RT_ENTER => sys_rt_enter(RawHandle(a1 as u32)), SYS_LOG_READ => { // checked_mul before mapping: a product that doesn't fit is a bad argument, diff --git a/kernel/src/syscall/io.rs b/kernel/src/syscall/io.rs index d231a185eba..e73820fc7c6 100644 --- a/kernel/src/syscall/io.rs +++ b/kernel/src/syscall/io.rs @@ -11,7 +11,7 @@ use crate::object::null::NullObject; use crate::object::{ops, KObjectRef}; use crate::time::{Cadence, Deadline, Duration}; use crate::user_ptr::{UserBytes, UserBytesMut}; -use crate::{device, pipe, process}; +use crate::{pipe, process}; use toyos_abi::handle::{RawHandle, Rights}; use toyos_abi::syscall::*; @@ -32,11 +32,8 @@ enum WriteBlock { /// What `sys_read` parks on when the handle has nothing to give. enum ReadBlock { Pipe(alloc::sync::Arc, pipe::PipeId, WaitClass), - /// A claimed keyboard, woken by its own IRQ. - Keyboard(Deadline), - /// A console, which reads the serial line and nothing else: it waits on - /// the serial line, never on the keyboard's queue, which only a claim - /// drains and which would answer it at once for as long as a key sits there. + /// A console, which reads the serial line and nothing else: nothing posts + /// a serial byte, so it parks on its own thread's watch until its re-poll. Console(Deadline), /// Nothing to wait for: the answer is this word. Refused(u64), @@ -89,21 +86,11 @@ pub(super) fn sys_write(h: RawHandle, buf: &UserBytes) -> u64 { } } -/// Only these device classes block; the rest answer `NotFound` on an -/// empty blocking read. -fn read_block_device(claim: &crate::object::device::DeviceClaim) -> ReadBlock { - match claim.class() { - device::DeviceType::Keyboard => ReadBlock::Keyboard(Deadline::never()), - _ => ReadBlock::Refused(SyscallError::NotFound.to_u64()), - } -} - fn read_block(object: &KObjectRef) -> ReadBlock { match object { KObjectRef::Device(_) => unreachable!("a device claim blocks via `read_block_device`"), KObjectRef::Console(_) => { - // Parks on `keyboard::WATCH` and re-polls: nothing posts a - // serial-console key, so the timer alone wakes it. + // Nothing posts a serial-console key, so the timer alone wakes it. const CONSOLE_REPOLL: Cadence = Cadence::every( Duration::from_millis(10), "nothing posts a serial-console key, so this rate is the whole of the wake", @@ -134,10 +121,10 @@ pub(super) fn sys_read(h: RawHandle, buf: &mut UserBytesMut) -> u64 { .handles .get::(h, Rights::READ) .expect("a Device resolved a moment ago under this same hold"); - let blocked = read_block_device(&claim); + // No device class blocks: an empty read answers `NotFound`. return match ops::read_device(&claim, &mut data.handles, buf) { Some(n) => Ok((n, None)), - None => Err(blocked), + None => Err(ReadBlock::Refused(SyscallError::NotFound.to_u64())), }; } match ops::try_read(object, buf) { @@ -165,26 +152,12 @@ pub(super) fn sys_read(h: RawHandle, buf: &mut UserBytesMut) -> u64 { return cancelled(); } } - Err(ReadBlock::Keyboard(deadline)) => { - let parkable = crate::scheduler::Parkable::at_entry(); - if watch::wait_until( - &parkable, - &crate::keyboard::WATCH, - 0, - WaitClass::Io, - deadline, - crate::keyboard::has_data, - ) - .is_err() - { - return cancelled(); - } - } Err(ReadBlock::Console(deadline)) => { let parkable = crate::scheduler::Parkable::at_entry(); + let Some(handle) = crate::sched::driver::current_handle() else { return cancelled() }; if watch::wait_until( &parkable, - &crate::keyboard::WATCH, + handle.watch(), 0, WaitClass::Io, deadline, diff --git a/kernel/src/syscall/machine.rs b/kernel/src/syscall/machine.rs index ffd5d4f2a20..679e4d89434 100644 --- a/kernel/src/syscall/machine.rs +++ b/kernel/src/syscall/machine.rs @@ -105,13 +105,6 @@ fn quiesce(last: &str) -> Result<(crate::quiesce::Stopping, StopWire), SyscallEr if crate::actuator::wedge_before_reset() { crate::deadline::stage_a_wedge(); } - // The same machine ended by the same bound, with the bus busy rather than - // idle: this one never stops writing, so the reset lands on a controller - // that is moving bytes. - #[cfg(feature = "boot-actuators")] - if crate::actuator::usb_reset_under_load() { - crate::usb_gate::sweep_under_load(); - } let parkable = crate::scheduler::Parkable::at_entry(); // The console's writer holding the wire as the stop begins, which a // shipping `klogd` does at any moment. @@ -141,10 +134,6 @@ fn quiesce(last: &str) -> Result<(crate::quiesce::Stopping, StopWire), SyscallEr } else { crate::alert!("{stopped}"); } - // Above the boot's last word, because these are ordinary records and the - // volume that carries them is still there: every USB disk's write cache is - // emptied and waited for before anything is taken down. - crate::drivers::xhci::flush_disks(); // `klogd` inside its hold of the wire as the last word is logged, which a // stop that did not hold the wire would leave it in at the seal below. #[cfg(feature = "boot-actuators")] @@ -165,16 +154,6 @@ fn quiesce(last: &str) -> Result<(crate::quiesce::Stopping, StopWire), SyscallEr // census and the last word among them. Nothing wrote them to `/log`, // and on a machine with no serial port this is their only reader. crate::log::seal_tail(stop_began); - // Whether or not the volume got them: a stick this boot's transport broke - // on is a stick the next host may not be able to read the log off. - crate::blackbox::append_recovery(); - // **The barrier, and last of all.** Below the sync, because before it the - // volumes still have bytes to take and this takes the controller away from - // them; and after everything else here, - // because nothing may run between it and the register stop - // `power::reboot`/`power::shutdown` do — which every reset this kernel - // performs goes through. It is bounded, and the reset follows either way. - crate::drivers::xhci::seal_shut(); #[cfg(feature = "boot-actuators")] if crate::actuator::wire_held_at_the_last_word() { crate::log::console::sealed(); diff --git a/kernel/src/time.rs b/kernel/src/time.rs index a258103ad05..e82238b6824 100644 --- a/kernel/src/time.rs +++ b/kernel/src/time.rs @@ -19,13 +19,6 @@ impl Instant { pub const fn from_nanos_since_boot(nanos: u64) -> Self { Self(nanos) } - - /// Leaves the type system that keeps an instant from being confused with a duration. - // Read only by the `boot-actuators` gate `sched_gate`. - #[cfg_attr(not(feature = "boot-actuators"), allow(dead_code))] - pub const fn nanos_since_boot(self) -> u64 { - self.0 - } } impl Add for Instant { @@ -169,10 +162,6 @@ impl Cadence { pub const fn duration(self) -> Duration { self.period } - - pub const fn nanos(self) -> u64 { - self.period.nanos() - } } impl fmt::Display for Cadence { diff --git a/kernel/src/tmpfs.rs b/kernel/src/tmpfs.rs index d16084900a9..c73c7c3cc93 100644 --- a/kernel/src/tmpfs.rs +++ b/kernel/src/tmpfs.rs @@ -20,7 +20,7 @@ struct TmpfsBacking { impl FileBacking for TmpfsBacking { /// `Err` once the file is deleted: a gone file is not a file of zeros. - fn read_page(&self, file_offset: u64, buf: &mut [u8; PAGE_BYTES]) -> crate::block::BlockResult { + fn read_page(&self, file_offset: u64, buf: &mut [u8; PAGE_BYTES]) -> crate::file_backing::BlockResult { // copy_page_out, not file_cache::read_page: reading through the miss path here would recurse. // A hole below the file size, left by a seek-and-write, reads as zero. if file_offset >= file_cache::size(self.file_id) @@ -29,7 +29,7 @@ impl FileBacking for TmpfsBacking { // After the miss: `retire` clears the flag before the pages drop. if !self.alive.load(Ordering::Acquire) { log!("tmpfs: read through a backing whose file was deleted"); - return Err(crate::block::BlockError::Device); + return Err(crate::file_backing::BlockError::Device); } buf.fill(0); } diff --git a/kernel/src/trace.rs b/kernel/src/trace.rs index 7a8f6a786c7..f7eec87f78d 100644 --- a/kernel/src/trace.rs +++ b/kernel/src/trace.rs @@ -1,6 +1,6 @@ //! The diary: a ring per CPU of `toyos_abi::trace` records, written always from -//! the timer, the scheduler, `irq_ring` and every -//! `kernel::sched::hw::TraceEvent` the core emits, and read by +//! the timer, the scheduler and every `kernel::sched::hw::TraceEvent` the core +//! emits, and read by //! `SYS_TRACE_READ` on `Rights::TRACE`. //! //! Each ring is the log's slot protocol ([`crate::log::shard::Ring`]) and is @@ -123,12 +123,6 @@ pub fn record(ev: TraceEvent) { push(kind, task.map(|name| ((name >> 32) as u32, name as u32)), data); } -/// Record consumption of an `irq_ring` record (see [`Kind::IrqDrain`]). -pub fn trace_irq_drain(source: crate::irq_ring::IrqSource, latency_us: u64) { - let data = ((source as u32) << 24) | (latency_us.min(0x00FF_FFFF) as u32); - trace(Kind::IrqDrain, data); -} - /// Every ring a reader names: one per CPU the firmware names. struct Diary; diff --git a/kernel/src/usb_gate.rs b/kernel/src/usb_gate.rs deleted file mode 100644 index 463372db12f..00000000000 --- a/kernel/src/usb_gate.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! `usb-reset-under-load`: the stick written continuously, so a reset lands on -//! a controller that is moving bytes. - -use alloc::vec; - -use crate::block::BlockDevice; -use crate::drivers::usb_storage; - -const BLOCK: usize = crate::mm::PAGE_SIZE as usize; - -/// Blocks per read-and-write-back pair — eight of this driver's largest SCSI -/// command, so each pair is several commands and not one. -#[cfg(feature = "boot-actuators")] -const WEDGE_CHUNK: u32 = 64; - -/// The smallest disk this load will sweep: a gibibyte in 4 KiB blocks. -/// -/// **A refusal and not a smaller sweep.** The sweep takes the last eighth of -/// the disk, which on anything smaller is inside a partition this kernel -/// mounts; a disk with no room for it is one this control cannot be staged on, -/// and saying so is the answer. -#[cfg(feature = "boot-actuators")] -const SWEEP_FLOOR: u64 = 262_144; - -/// Stream writes to the stick until something else ends the machine, so the -/// reset lands on a controller that is moving bytes and a device that is -/// programming flash. -/// -/// **The last eighth once and never twice.** Every run is read first and -/// written back byte for byte, so the medium is what it was however much of a -/// run either reset completes; and the sweep stops at the end of that span -/// rather than wrapping, so no block on the owner's stick is programmed twice -/// in a boot. A sweep that reaches the end says so by name, because a bus that -/// went idle before the reset is the idle case again under this arm's name. -#[cfg(feature = "boot-actuators")] -pub fn sweep_under_load() { - let Some((disk, _)) = usb_storage::handle(0) else { - log!("{LOAD_REFUSED}: no USB disk on this machine"); - return; - }; - let blocks = disk.block_count(); - if blocks < SWEEP_FLOOR { - log!("{LOAD_REFUSED}: disk 0 holds {blocks} blocks and this load sweeps the last \ - eighth of a disk of at least {SWEEP_FLOOR}"); - return; - } - let first = blocks - blocks / 8; - log!("{LOAD_RUNNING} from block {first} to {blocks}, rewriting each run with the bytes \ - just read from it, until this machine is reset out from under it"); - // `IF` on and preemption off: the bound that has to end this machine is the - // boot deadline, polled from the timer entry, and a CPU that takes no - // interrupt at all is a hard lockup ended half a bound earlier by a - // different mechanism under this arm's name. - crate::preempt::disable(); - crate::arch::irqchip::arm_within(kernel::sched::fair::QUANTUM_NS); - let mut buf = vec![0u8; WEDGE_CHUNK as usize * BLOCK]; - let mut at = first; - let mut stopped = false; - while at + u64::from(WEDGE_CHUNK) <= blocks { - if disk.lock().read_blocks(at, WEDGE_CHUNK, &mut buf).is_err() - || disk.lock().write_blocks(at, WEDGE_CHUNK, &buf).is_err() - { - log!("{LOAD_STOPPED} at block {at}"); - stopped = true; - break; - } - at += u64::from(WEDGE_CHUNK); - } - if !stopped { - log!("{LOAD_SWEPT} at block {at}, so the bus is idle for the rest of this boot"); - } - // Put back on the one path out of here, because the caller goes on to drain - // write-back, sync every filesystem, flush every disk and wait for the log - // to be durable, and none of that may run under a preempt count this left - // behind. Not `preempt::enable`: the request stays set and the caller's own - // next preemption point serves it, rather than a scheduler pass taken from - // inside the shutdown syscall. - crate::preempt::enable_no_resched(); -} - -/// What the load says when it starts, when it cannot, when the disk stopped -/// answering it, and when it reached the end of its one pass. Judged by the -/// harness, so all four are constants (`build/bootlog.rs`). -#[cfg(feature = "boot-actuators")] -pub const LOAD_RUNNING: &str = "usb-load: sweeping disk 0"; -#[cfg(feature = "boot-actuators")] -pub const LOAD_REFUSED: &str = "usb-load: refused"; -#[cfg(feature = "boot-actuators")] -pub const LOAD_STOPPED: &str = "usb-load: the disk stopped answering"; -#[cfg(feature = "boot-actuators")] -pub const LOAD_SWEPT: &str = "usb-load: the sweep reached the end of the disk"; diff --git a/kernel/usbhid/Cargo.toml b/kernel/usbhid/Cargo.toml deleted file mode 100644 index b586e5b5deb..00000000000 --- a/kernel/usbhid/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -# A member of the host workspace (root `Cargo.toml`): the kernel depends on it -# by path and its tests run on the host. - -[package] -name = "toyos-usbhid" -description = "USB HID boot keyboard and pointer reports in, key transitions and pointer motion out, every malformed report refused by name; pure." -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 deleted file mode 100644 index 1af92bbf823..00000000000 --- a/kernel/usbhid/src/keyboard.rs +++ /dev/null @@ -1,390 +0,0 @@ -//! The boot keyboard report (HID 1.11 Appendix B.1), as the set of keys it -//! holds. -//! -//! ```text -//! byte 0 modifier keys as bits: bit n is usage 0xE0 + n (LeftControl .. RightGUI) -//! byte 1 reserved for the maker, read by nobody -//! bytes 2-7 Keyboard/Keypad page usages held, 0 in a slot holding none -//! ``` -//! -//! A report is everything held, so a transition is the difference between two -//! reports of **one** keyboard: diffing against another's would release keys -//! still down. A usage held twice — in two slots, or as a bit and in a slot — -//! is held once. -//! -//! Usages 1 to 3 are not keys but the keyboard saying what it cannot say -//! (HID Usage Tables, Keyboard/Keypad page). ErrorRollOver is Appendix C's -//! phantom state, which a keyboard reports in all array fields when more keys -//! are down than the slots can name; byte 0 is a variable field and not an -//! array, and Appendix C has it reported as it stands. So such a report's -//! modifiers are believed and its slots are not, and the other keys held -//! before stay held: [`Read::RollOver`]. POSTFail and ErrorUndefined are the -//! keyboard reporting a fault, and the whole report is refused. -//! -//! Every other slot usage is a key, past 0x65 and into the reserved -//! 0xE8..=0xFF too: Appendix B.1 declares the slots an array of Logical -//! Maximum 255 over Usage Minimum 0 to Usage Maximum 255, so every byte a slot -//! can hold names a usage. 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; - -const ERROR_ROLL_OVER: u8 = 0x01; -const POST_FAIL: u8 = 0x02; -const ERROR_UNDEFINED: u8 = 0x03; - -/// Why a report was not believed at all; the keyboard's state is the last -/// report that was. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Refused { - /// Not the [`REPORT`] bytes of a boot report. - Length(usize), - /// POSTFail in a slot: the keyboard failed its own power-on test. - PostFail, - /// ErrorUndefined in a slot: an error the keyboard does not name. - Undefined, -} - -/// What a believed report did. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Read { - /// It named every key down. - Keys(Transitions), - /// ErrorRollOver in a slot: more keys are down than the slots can name, so - /// only its modifiers were believed. - RollOver(Transitions), -} - -/// One boot keyboard: the keys its last believed report held, and by -/// `Default` none, as when it binds. -/// -/// Not `Copy`: a copy would hold keys the keyboard itself goes on to release. -#[derive(PartialEq, Eq, Debug, Default)] -#[cfg_attr(test, derive(Clone))] -pub struct Keyboard { - keys: Keys, -} - -impl Keyboard { - /// The transitions `report` makes from the last report believed, or why it - /// is not believed. - pub fn report(&mut self, report: &[u8]) -> Result { - let report: &[u8; REPORT] = report.try_into().map_err(|_| Refused::Length(report.len()))?; - let modifiers = Usages::modifier_byte(report[0]); - let mut now = modifiers; - let mut rolled_over = false; - for &usage in &report[2..] { - match usage { - 0 => {} - ERROR_ROLL_OVER => rolled_over = true, - POST_FAIL => return Err(Refused::PostFail), - ERROR_UNDEFINED => return Err(Refused::Undefined), - usage => now.insert(usage), - } - } - if rolled_over { - let kept = self.keys.held().keys().union(modifiers); - return Ok(Read::RollOver(self.keys.hold(kept))); - } - 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.keys.hold(Usages::NONE) - } -} - -#[cfg(test)] -pub(crate) mod tests { - extern crate std; - use super::*; - use std::collections::BTreeSet; - use std::vec::Vec; - - fn press(usage: u8) -> Transition { - Transition { usage, pressed: true } - } - - fn lift(usage: u8) -> Transition { - Transition { usage, pressed: false } - } - - /// The most transitions one report can make: the eight modifiers, and six - /// slots released and six pressed. - const MOST: usize = 20; - - /// What a report read as, with its transitions collected. - #[derive(PartialEq, Eq, Debug)] - enum Got { - Keys(Vec), - RollOver(Vec), - } - - fn got(keyboard: &mut Keyboard, report: &[u8]) -> Result { - keyboard.report(report).map(|read| match read { - Read::Keys(made) => Got::Keys(made.collect()), - Read::RollOver(made) => Got::RollOver(made.collect()), - }) - } - - /// A report that names every key down. - fn fed(keyboard: &mut Keyboard, report: &[u8]) -> Result, Refused> { - got(keyboard, report).map(|got| match got { - Got::Keys(made) => made, - Got::RollOver(made) => panic!("{report:02x?} rolled over, making {made:?}"), - }) - } - - /// A report in the phantom state. - fn rolled(keyboard: &mut Keyboard, report: &[u8]) -> Vec { - match got(keyboard, report) { - Ok(Got::RollOver(made)) => made, - other => panic!("{report:02x?} did not roll over: {other:?}"), - } - } - - /// Appendix B.1 byte for byte: bit n of byte 0 is usage 0xE0 + n, byte 1 - /// is nobody's, and a slot names its usage. - #[test] - fn a_report_is_read_as_appendix_b_lays_it_out() { - for n in 0..8u8 { - let mut keyboard = Keyboard::default(); - assert_eq!(fed(&mut keyboard, &[1 << n, 0, 0, 0, 0, 0, 0, 0]), Ok(std::vec![press(0xE0 + n)])); - } - let mut keyboard = Keyboard::default(); - assert_eq!(fed(&mut keyboard, &[0, 0xFF, 0, 0, 0, 0, 0, 0]), Ok(std::vec![]), "byte 1 is reserved"); - for slot in 2..REPORT { - let mut keyboard = Keyboard::default(); - let mut report = [0u8; REPORT]; - report[slot] = 0x04; - assert_eq!(fed(&mut keyboard, &report), Ok(std::vec![press(0x04)]), "slot {slot}"); - } - } - - /// The order a reader of the stream depends on: Shift before the key it - /// shifts, and a release before the press that took its slot. - #[test] - fn modifiers_come_first_then_releases_then_presses() { - let mut keyboard = Keyboard::default(); - assert_eq!(fed(&mut keyboard, &[0x02, 0, 0x05, 0, 0, 0, 0, 0]), Ok(std::vec![press(0xE1), press(0x05)])); - assert_eq!( - fed(&mut keyboard, &[0x00, 0, 0x06, 0, 0, 0, 0, 0]), - Ok(std::vec![lift(0xE1), lift(0x05), press(0x06)]) - ); - assert_eq!( - fed(&mut keyboard, &[0x05, 0, 0x07, 0, 0, 0, 0, 0]), - Ok(std::vec![press(0xE0), press(0xE2), lift(0x06), press(0x07)]), - "Ctrl+Alt+D in one report is the chord" - ); - assert_eq!(fed(&mut keyboard, &[0x05, 0, 0x07, 0, 0, 0, 0, 0]), Ok(std::vec![]), "nothing changed"); - } - - /// A key that moves slot is not a release and a press; a usage held twice - /// is held once and released once. - #[test] - fn a_report_is_a_set_of_keys_and_not_a_list_of_slots() { - let mut keyboard = Keyboard::default(); - assert_eq!(fed(&mut keyboard, &[0, 0, 0x04, 0x05, 0, 0, 0, 0]), Ok(std::vec![press(0x04), press(0x05)])); - assert_eq!(fed(&mut keyboard, &[0, 0, 0x05, 0x04, 0, 0, 0, 0]), Ok(std::vec![])); - assert_eq!(fed(&mut keyboard, &[0, 0, 0x05, 0x05, 0x04, 0x05, 0, 0]), Ok(std::vec![])); - assert_eq!(fed(&mut keyboard, &[0x01, 0, 0xE0, 0, 0, 0, 0, 0]), Ok(std::vec![press(0xE0), lift(0x04), lift(0x05)])); - assert_eq!(fed(&mut keyboard, &[0x01, 0, 0, 0, 0, 0, 0, 0]), Ok(std::vec![]), "still held by its bit"); - assert_eq!(fed(&mut keyboard, &[0; REPORT]), Ok(std::vec![lift(0xE0)])); - } - - /// POSTFail and ErrorUndefined anywhere in the six slots, ErrorRollOver - /// beside them or not: refused, and the keys held before are still held. - #[test] - fn a_fault_usage_is_refused_and_the_keys_held_stay_held() { - for (usage, why) in [(2, Refused::PostFail), (3, Refused::Undefined)] { - for slot in 2..REPORT { - for beside in [0x04, 1] { - let mut keyboard = Keyboard::default(); - fed(&mut keyboard, &[0x02, 0, 0x04, 0x05, 0, 0, 0, 0]).expect("a good report"); - let before = keyboard.clone(); - let mut report = [0x00, 0, beside, beside, beside, beside, beside, beside]; - report[slot] = usage; - assert_eq!(got(&mut keyboard, &report), Err(why), "usage {usage} in slot {slot}"); - assert_eq!(keyboard, before, "a refused report changed what is held"); - } - } - } - } - - /// Appendix C's phantom state anywhere in the six slots: the slots are not - /// believed, so the keys held stay held, and byte 0 is, so a modifier - /// moves — released, or pressed and released inside the phantom state. - #[test] - fn a_rollover_report_believes_its_modifiers_and_not_its_slots() { - for slot in 2..REPORT { - let mut keyboard = Keyboard::default(); - fed(&mut keyboard, &[0x02, 0, 0x04, 0x05, 0, 0, 0, 0]).expect("a good report"); - let mut report = [0x00, 0, 0x06, 0, 0, 0, 0, 0]; - report[slot] = 1; - assert_eq!(rolled(&mut keyboard, &report), std::vec![lift(0xE1)], "rollover in slot {slot}"); - assert_eq!(fed(&mut keyboard, &[0, 0, 0x04, 0x05, 0, 0, 0, 0]), Ok(std::vec![]), "slot {slot}"); - } - let mut keyboard = Keyboard::default(); - fed(&mut keyboard, &[0x02, 0, 0x04, 0x05, 0, 0, 0, 0]).expect("a good report"); - assert_eq!(rolled(&mut keyboard, &[0x00, 0, 1, 1, 1, 1, 1, 1]), std::vec![lift(0xE1)]); - assert_eq!(rolled(&mut keyboard, &[0x01, 0, 1, 1, 1, 1, 1, 1]), std::vec![press(0xE0)]); - assert_eq!(rolled(&mut keyboard, &[0x00, 0, 1, 1, 1, 1, 1, 1]), std::vec![lift(0xE0)]); - assert_eq!(rolled(&mut keyboard, &[0x00, 0, 1, 1, 1, 1, 1, 1]), std::vec![]); - assert_eq!( - fed(&mut keyboard, &[0x00, 0, 0x05, 0, 0, 0, 0, 0]), - Ok(std::vec![lift(0x04)]), - "the next whole report is diffed against the keys held through the rollover" - ); - } - - #[test] - fn a_report_of_any_other_length_is_refused() { - for len in (0..REPORT).chain(REPORT + 1..=64) { - let mut keyboard = Keyboard::default(); - fed(&mut keyboard, &[0, 0, 0x04, 0, 0, 0, 0, 0]).expect("a good report"); - let before = keyboard.clone(); - let report = std::vec![0x02; len]; - assert_eq!(fed(&mut keyboard, &report), Err(Refused::Length(len))); - assert_eq!(keyboard, before); - } - } - - #[test] - fn a_keyboard_leaving_releases_everything_it_held_and_nothing_else() { - let mut keyboard = Keyboard::default(); - fed(&mut keyboard, &[0x81, 0xFF, 0x04, 0xFF, 0x65, 0, 0, 0]).expect("a good report"); - let released: Vec<_> = keyboard.release().collect(); - assert_eq!(released, std::vec![lift(0xE0), lift(0xE7), lift(0x04), lift(0x65), lift(0xFF)]); - assert_eq!(keyboard.release().count(), 0); - } - - /// Every report QEMU 11.1's `usb-kbd` delivered to the kernel's driver - /// over xHCI, each transfer whole, for QMP key events: `a` down and up, - /// Shift+B, then Q W E R T Y U down, U up, and the rest up. The seventh key - /// is ErrorRollOver in every slot, and reading it as keys would release - /// the six still held and press them again on the next report. - #[test] - fn reports_qemu_sent_read_as_the_keys_that_were_pressed() { - let mut keyboard = Keyboard::default(); - let captured: &[(&[u8], Result)] = &[ - (&[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00], Ok(Got::Keys(std::vec![press(0x04)]))), - (&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], Ok(Got::Keys(std::vec![lift(0x04)]))), - (&[0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], Ok(Got::Keys(std::vec![press(0xE1)]))), - (&[0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00], Ok(Got::Keys(std::vec![press(0x05)]))), - (&[0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], Ok(Got::Keys(std::vec![lift(0x05)]))), - (&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], Ok(Got::Keys(std::vec![lift(0xE1)]))), - (&[0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00], Ok(Got::Keys(std::vec![press(0x14)]))), - (&[0x00, 0x00, 0x14, 0x1A, 0x00, 0x00, 0x00, 0x00], Ok(Got::Keys(std::vec![press(0x1A)]))), - (&[0x00, 0x00, 0x14, 0x1A, 0x08, 0x00, 0x00, 0x00], Ok(Got::Keys(std::vec![press(0x08)]))), - (&[0x00, 0x00, 0x14, 0x1A, 0x08, 0x15, 0x00, 0x00], Ok(Got::Keys(std::vec![press(0x15)]))), - (&[0x00, 0x00, 0x14, 0x1A, 0x08, 0x15, 0x17, 0x00], Ok(Got::Keys(std::vec![press(0x17)]))), - (&[0x00, 0x00, 0x14, 0x1A, 0x08, 0x15, 0x17, 0x1C], Ok(Got::Keys(std::vec![press(0x1C)]))), - (&[0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01], Ok(Got::RollOver(std::vec![]))), - (&[0x00, 0x00, 0x14, 0x1A, 0x08, 0x15, 0x17, 0x1C], Ok(Got::Keys(std::vec![]))), - (&[0x00, 0x00, 0x1C, 0x1A, 0x08, 0x15, 0x17, 0x00], Ok(Got::Keys(std::vec![lift(0x14)]))), - (&[0x00, 0x00, 0x1C, 0x17, 0x08, 0x15, 0x00, 0x00], Ok(Got::Keys(std::vec![lift(0x1A)]))), - (&[0x00, 0x00, 0x1C, 0x17, 0x15, 0x00, 0x00, 0x00], Ok(Got::Keys(std::vec![lift(0x08)]))), - (&[0x00, 0x00, 0x1C, 0x17, 0x00, 0x00, 0x00, 0x00], Ok(Got::Keys(std::vec![lift(0x15)]))), - (&[0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00], Ok(Got::Keys(std::vec![lift(0x17)]))), - (&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], Ok(Got::Keys(std::vec![lift(0x1C)]))), - ]; - for (at, (report, want)) in captured.iter().enumerate() { - assert_eq!(&got(&mut keyboard, report), want, "report {at}: {report:02x?}"); - } - } - - /// A small generator with no dependency: xorshift64, seeded per test so a - /// failure names the case that made it. - pub(crate) struct Bytes(u64); - - impl Bytes { - pub(crate) fn new(seed: u64) -> Self { - Self(seed | 1) - } - - pub(crate) fn next(&mut self) -> u64 { - self.0 ^= self.0 << 13; - self.0 ^= self.0 >> 7; - self.0 ^= self.0 << 17; - self.0 - } - - /// A byte drawn half the time from `edges` and otherwise from any. - pub(crate) fn byte(&mut self, edges: &[u8]) -> u8 { - let roll = self.next(); - if roll & 1 == 0 { - edges[(roll >> 1) as usize % edges.len()] - } else { - (roll >> 8) as u8 - } - } - } - - /// What a report reads as, written as a reader of Appendix B.1 and C would - /// and not as [`Keyboard::report`] does: the oracle the fuzz holds it to. - /// `None` is a refusal; otherwise the set held after it, from `held` - /// before it, and whether it rolled over. - fn read_by(report: &[u8], held: &BTreeSet) -> Option<(BTreeSet, bool)> { - let slots = report.get(2..).filter(|_| report.len() == REPORT)?; - if slots.iter().any(|&u| u == 2 || u == 3) { - return None; - } - let modifiers = (0..8).filter(|n| report[0] & (1 << n) != 0).map(|n| 0xE0 + n); - if slots.contains(&1) { - let keys = held.iter().copied().filter(|u| !(0xE0..=0xE7).contains(u)); - return Some((keys.chain(modifiers).collect(), true)); - } - Some((modifiers.chain(slots.iter().copied().filter(|&u| u != 0)).collect(), false)) - } - - /// Every report, of any length and any bytes, is believed or refused by - /// name; a believed one moves the keyboard to exactly the set the oracle - /// says, one transition per usage that changed, and a refused one moves - /// nothing. - #[test] - fn every_report_is_believed_or_refused_and_nothing_else() { - const EDGES: &[u8] = &[0, 1, 2, 3, 4, 0x39, 0x65, 0x66, 0xDF, 0xE0, 0xE3, 0xE7, 0xE8, 0xFF]; - let mut bytes = Bytes::new(0x7573_6268_6964); - let mut keyboard = Keyboard::default(); - let mut held = BTreeSet::new(); - let (mut whole, mut rolled, mut refused) = (0, 0, 0); - for case in 0..200_000 { - let len = if bytes.next().is_multiple_of(8) { (bytes.next() % 12) as usize } else { REPORT }; - let report: Vec = (0..len).map(|_| bytes.byte(EDGES)).collect(); - let before = keyboard.clone(); - match (keyboard.report(&report), read_by(&report, &held)) { - (Ok(read), Some((now, rolled_over))) => { - let transitions = match read { - Read::Keys(transitions) => { - whole += 1; - transitions - } - Read::RollOver(transitions) => { - rolled += 1; - transitions - } - }; - assert_eq!(matches!(read, Read::RollOver(_)), rolled_over, "case {case}: {report:02x?}"); - let made: Vec = transitions.collect(); - assert!(made.len() <= MOST, "case {case}: {} transitions", made.len()); - let changed: BTreeSet = held.symmetric_difference(&now).copied().collect(); - let named: BTreeSet = made.iter().map(|t| t.usage).collect(); - assert_eq!(named.len(), made.len(), "case {case}: a usage twice in {made:?}"); - assert_eq!(named, changed, "case {case}: {report:02x?}"); - assert!(made.iter().all(|t| t.pressed == now.contains(&t.usage)), "case {case}"); - held = now; - } - (Err(why), None) => { - refused += 1; - assert_eq!(keyboard, before, "case {case}: {why:?} moved the keyboard"); - } - (read, want) => panic!("case {case}: {report:02x?} read as {read:?}, the oracle says {want:?}"), - } - } - assert!(whole > 10_000 && rolled > 10_000 && refused > 10_000, "{whole} whole, {rolled} rolled, {refused} refused"); - } -} diff --git a/kernel/usbhid/src/lib.rs b/kernel/usbhid/src/lib.rs deleted file mode 100644 index 6e0ab05a881..00000000000 --- a/kernel/usbhid/src/lib.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! USB HID reports in, key transitions and pointer motion out. -//! -//! Every byte here is one a device chose. A report that does not read as the -//! layout its interface promised is refused by name and changes no state, and -//! no input panics: [`keyboard::Keyboard::report`] and -//! [`pointer::Pointer::decode`] are total over every byte string. -//! -//! The layouts are fixed by the interface's protocol and read from no report -//! descriptor: HID 1.11 Appendix B's boot keyboard and boot mouse, and QEMU's -//! `usb-tablet`, which is what the kernel binds a subclass-0 interface as. -//! Nothing here touches a device, allocates, or holds a lock. - -#![no_std] -#![forbid(unsafe_code)] - -pub mod keyboard; -pub mod pointer; diff --git a/kernel/usbhid/src/pointer.rs b/kernel/usbhid/src/pointer.rs deleted file mode 100644 index 7e6845bfa42..00000000000 --- a/kernel/usbhid/src/pointer.rs +++ /dev/null @@ -1,224 +0,0 @@ -//! Pointer reports: the boot mouse (HID 1.11 Appendix B.2) and QEMU's -//! `usb-tablet`. -//! -//! ```text -//! mouse byte 0 buttons (bit 0 left, 1 right, 2 middle, the rest the maker's) -//! byte 1 X, byte 2 Y: signed displacements, Y positive towards the user -//! byte 3 the wheel, where the mouse sends one (not B.2's; see below) -//! tablet byte 0 buttons, bytes 1-2 X and 3-4 Y little endian, 0 to 0x7FFF, -//! byte 5 the wheel -//! ``` -//! -//! 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; 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; - -/// Which report an interface sends, fixed by its protocol when it binds. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Pointer { - Mouse, - Tablet, -} - -/// Where a report moves the pointer. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Motion { - Relative { dx: i8, dy: i8 }, - Absolute { x: u16, y: u16 }, -} - -/// One report, read. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Report { - pub buttons: u8, - pub motion: Motion, - pub wheel: i8, -} - -/// Why a report was not believed. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Refused { - /// Too short for its layout, or longer than its transfer asks for. - Length(usize), - /// A tablet position past [`MOST`]: off the surface. - OffTheSurface { x: u16, y: u16 }, -} - -impl Pointer { - /// What the interrupt transfer asks for: the longest report this layout - /// reads. - pub const fn request(self) -> usize { - match self { - Self::Mouse => 4, - Self::Tablet => 6, - } - } - - /// `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, - motion: Motion::Relative { dx: dx as i8, dy: dy as i8 }, - wheel: 0, - }), - (Self::Mouse, &[buttons, dx, dy, wheel]) => Ok(Report { - buttons, - motion: Motion::Relative { dx: dx as i8, dy: dy as i8 }, - wheel: wheel as i8, - }), - (Self::Tablet, &[buttons, x0, x1, y0, y1, wheel]) => { - let (x, y) = (u16::from_le_bytes([x0, x1]), u16::from_le_bytes([y0, y1])); - if x > MOST || y > MOST { - return Err(Refused::OffTheSurface { x, y }); - } - Ok(Report { buttons, motion: Motion::Absolute { x, y }, wheel: wheel as i8 }) - } - (_, report) => Err(Refused::Length(report.len())), - } - } -} - -#[cfg(test)] -mod tests { - extern crate std; - use super::*; - use crate::keyboard::tests::Bytes; - use std::vec::Vec; - - fn mouse(buttons: u8, dx: i8, dy: i8, wheel: i8) -> Result { - Ok(Report { buttons, motion: Motion::Relative { dx, dy }, wheel }) - } - - fn tablet(buttons: u8, x: u16, y: u16, wheel: i8) -> Result { - Ok(Report { buttons, motion: Motion::Absolute { x, y }, wheel }) - } - - /// Appendix B.2: displacements are signed bytes, so 0x80 is -128 and not - /// 128, and a mouse with no wheel sends three bytes. - #[test] - 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)); - } - - /// 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] - fn a_tablet_report_is_its_position_little_endian() { - assert_eq!(Pointer::Tablet.decode(&[0x02, 0x34, 0x12, 0xFF, 0x7F, 0xFF]), tablet(0x02, 0x1234, 0x7FFF, -1)); - assert_eq!(Pointer::Tablet.decode(&[0, 0, 0, 0, 0, 0]), tablet(0, 0, 0, 0)); - } - - #[test] - fn a_tablet_position_off_the_surface_is_refused() { - assert_eq!( - Pointer::Tablet.decode(&[0, 0x00, 0x80, 0, 0, 0]), - Err(Refused::OffTheSurface { x: 0x8000, y: 0 }) - ); - assert_eq!( - Pointer::Tablet.decode(&[0, 0, 0, 0xFF, 0xFF, 0]), - Err(Refused::OffTheSurface { x: 0, y: 0xFFFF }) - ); - } - - /// A short transfer is not the report with its tail as it was: those bytes - /// are the last report's. - #[test] - fn a_report_of_a_length_its_layout_does_not_have_is_refused() { - for len in 0..=16 { - let report = std::vec![0u8; len]; - let mouse = Pointer::Mouse.decode(&report); - assert_eq!(mouse.is_ok(), len == 3 || len == 4, "mouse, {len} bytes: {mouse:?}"); - let tablet = Pointer::Tablet.decode(&report); - assert_eq!(tablet.is_ok(), len == 6, "tablet, {len} bytes: {tablet:?}"); - if len > Pointer::Mouse.request() || len < 3 { - assert_eq!(mouse, Err(Refused::Length(len))); - } - if len != Pointer::Tablet.request() { - assert_eq!(tablet, Err(Refused::Length(len))); - } - } - } - - /// Every report QEMU 11.1's `usb-mouse` and `usb-tablet` delivered to the - /// kernel's driver over xHCI, each transfer whole, for QMP events: a move - /// of (5, -5), one of (-200, 200), which QEMU splits at a byte's range, - /// the left button down and up, a wheel notch, and the tablet at - /// (0x1234, 0x7FFF) and (0, 0). - #[test] - fn reports_qemu_sent_read_as_the_motion_that_was_sent() { - use Pointer::{Mouse, Tablet}; - let captured: &[(Pointer, &[u8], Result)] = &[ - (Mouse, &[0x00, 0x05, 0xFB, 0x00], mouse(0, 5, -5, 0)), - (Mouse, &[0x00, 0x81, 0x7F, 0x00], mouse(0, -127, 127, 0)), - (Mouse, &[0x00, 0xB7, 0x49, 0x00], mouse(0, -73, 73, 0)), - (Mouse, &[0x01, 0x00, 0x00, 0x00], mouse(1, 0, 0, 0)), - (Mouse, &[0x00, 0x00, 0x00, 0x00], mouse(0, 0, 0, 0)), - (Mouse, &[0x00, 0x00, 0x00, 0x01], mouse(0, 0, 0, 1)), - (Tablet, &[0x00, 0x34, 0x12, 0xFF, 0x7F, 0x00], tablet(0, 0x1234, 0x7FFF, 0)), - (Tablet, &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00], tablet(0, 0, 0, 0)), - ]; - for &(pointer, report, want) in captured { - assert_eq!(pointer.decode(report), want, "{pointer:?} {report:02x?}"); - } - } - - /// Every report is read or refused by name, and a read one says what its - /// bytes say: a tablet's position is the bytes it came in, and never past - /// the surface. - #[test] - fn every_report_is_read_or_refused_and_nothing_else() { - const EDGES: &[u8] = &[0, 1, 0x7F, 0x80, 0x81, 0xFE, 0xFF]; - let mut bytes = Bytes::new(0x706f_696e_7465); - for case in 0..200_000 { - let len = (bytes.next() % 9) as usize; - let report: Vec = (0..len).map(|_| bytes.byte(EDGES)).collect(); - for pointer in [Pointer::Mouse, Pointer::Tablet] { - match pointer.decode(&report) { - Ok(read) => { - assert!(len <= pointer.request(), "case {case}: {pointer:?} read {len} bytes"); - assert_eq!(read.buttons, report[0] & BUTTONS, "case {case}"); - match read.motion { - Motion::Relative { dx, dy } => { - assert_eq!(pointer, Pointer::Mouse, "case {case}"); - assert_eq!((dx as u8, dy as u8), (report[1], report[2]), "case {case}"); - } - Motion::Absolute { x, y } => { - assert_eq!(pointer, Pointer::Tablet, "case {case}"); - assert!(x <= MOST && y <= MOST, "case {case}: ({x}, {y})"); - assert_eq!(x.to_le_bytes(), [report[1], report[2]], "case {case}"); - assert_eq!(y.to_le_bytes(), [report[3], report[4]], "case {case}"); - } - } - } - Err(Refused::Length(n)) => assert_eq!(n, len, "case {case}"), - Err(Refused::OffTheSurface { x, y }) => { - assert_eq!(pointer, Pointer::Tablet, "case {case}"); - assert!(x > MOST || y > MOST, "case {case}: ({x}, {y}) refused"); - } - } - } - } - } -} diff --git a/lib/bcachefs/tests/integration.rs b/lib/bcachefs/tests/integration.rs index cc4fb2273c1..5a3e5709c50 100644 --- a/lib/bcachefs/tests/integration.rs +++ b/lib/bcachefs/tests/integration.rs @@ -1336,7 +1336,7 @@ impl bcachefs::BlockIO for Postpones { /// The retry discriminant crosses this crate unchanged: a `Refused` sync comes /// out of `Mounted::sync` still `Refused`, never widened into the device's own -/// word — the erasure `kernel/CLAUDE.md`'s BudgetExpired rule forbids. +/// word. #[test] fn a_refused_sync_stays_refused_through_the_filesystem() { let raw = volume_with("doc.bin", b"small"); diff --git a/lib/blackbox/src/lib.rs b/lib/blackbox/src/lib.rs index 28553c3f207..819d5d9442b 100644 --- a/lib/blackbox/src/lib.rs +++ b/lib/blackbox/src/lib.rs @@ -280,7 +280,7 @@ impl<'s, W: core::fmt::Write> Whole<'s, W> { } /// The most a death's census may spend of the record it is sealed into: a -/// quarter of the box, as [`RECOVERY_BYTES`] is, so that what a kernel built +/// quarter of the box, so that what a kernel built /// for more CPUs says of them comes off the census and never off the crash /// above it or the ring's tail below. pub const CENSUS_BYTES: usize = 4096; @@ -506,218 +506,6 @@ impl<'a, F: FnMut()> Account<'a, F> { } } -/// What every line of a record's recovery section opens with: the USB stack's -/// own records from the boot's first transport break on, oldest first. -/// -/// **Apart from the tail, because the tail is the newest records and a break is -/// not.** A boot that outlives its transport's break by a minute has a minute -/// of other records above it, and the cut that fits them to the page takes the -/// break first — which is the record that says what happened to the device. -/// Never a record's own opening ([`RECORD_OPENS_WITH`]), so the loader prints -/// these as head and a reader can tell the section from the tail under it. -pub const RECOVERY_OPENS_WITH: &str = "usb-recovery: "; - -/// The line that stands for the section on a boot whose ring holds no break, -/// so a record without the section is told from one written before it existed. -pub const RECOVERY_NONE: &str = "usb-recovery: the log ring holds no transport break"; - -/// The most the section's records may spend: a quarter of the box, off the tail -/// and never off [`ACCOUNT_BYTES`]. -pub const RECOVERY_BYTES: usize = 4096; - -const RECOVERY_KEPT_OF: &str = " of the "; -const RECOVERY_COUNTED: &str = - " record(s) the USB stack wrote from its first transport break on, oldest first"; - -/// Room kept ahead of the section's records for the line that counts them, at -/// the widest two `u64`s can make it: the counts are known only once the -/// records are placed, and the line goes above them. -const RECOVERY_HEAD_BYTES: usize = - RECOVERY_OPENS_WITH.len() + 20 + RECOVERY_KEPT_OF.len() + 20 + RECOVERY_COUNTED.len() + 1; - -/// The heads the USB stack's drivers write their records under. -const USB_HEADS: [&str; 2] = ["usb-storage: ", "xHCI: "]; - -/// What a transport break's record says, in both spellings the driver has: -/// `transport broke on SCSI …` and `broke on TEST UNIT READY`. -const BREAK_SAYS: &str = " broke on "; - -/// Whether the USB stack wrote this record. -pub fn usb_wrote(message: &str) -> bool { - USB_HEADS.iter().any(|head| message.starts_with(head)) -} - -/// Whether this record is a transport break's. -pub fn says_a_break(message: &str) -> bool { - usb_wrote(message) && message.contains(BREAK_SAYS) -} - -/// What one record's line costs in the section. -fn recovery_line_bytes(line: &impl core::fmt::Display) -> usize { - bytes_of(format_args!("{RECOVERY_OPENS_WITH}{line}\n")) -} - -/// The first of the section's two walks over the log ring, **newest record -/// first**: where the boot's first break is, and what every USB record from it -/// on would cost. -/// -/// Newest first because that is the only order the ring's lock-free reader has, -/// and the seals this serves run where no other reader may. The oldest break is -/// therefore the last one seen, and the sums taken at it are the section's. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Recovery { - /// The oldest break's stamp, or `None` while none has been seen. - from: Option, - /// USB records, and their lines' bytes, from the newest down to the record - /// last seen. - seen: (u64, usize), - /// The same two down to the oldest break: the section, were it kept whole. - whole: (u64, usize), -} - -impl Recovery { - pub const fn new() -> Self { - Self { from: None, seen: (0, 0), whole: (0, 0) } - } - - /// One record of the walk: its stamp, its message, and what renders it. - pub fn saw(&mut self, at_ns: u64, message: &str, line: &impl core::fmt::Display) { - if !usb_wrote(message) { - return; - } - self.seen = ( - self.seen.0.saturating_add(1), - self.seen.1.saturating_add(recovery_line_bytes(line)), - ); - if says_a_break(message) { - self.from = Some(self.from.map_or(at_ns, |from| from.min(at_ns))); - self.whole = self.seen; - } - } - - /// The stamp the second walk starts from, or `None` on a boot with no break. - pub fn from(&self) -> Option { - self.from - } -} - -impl Default for Recovery { - fn default() -> Self { - Self::new() - } -} - -/// The section being placed into a report by its second walk, **newest record -/// first** again, over the records stamped from [`Recovery::from`] on. -/// -/// **The oldest that fit are the ones kept**, because the break and the -/// recovery that answered it are what the section is for and the newest records -/// are the tail's already. Each record goes straight to where it ends up — -/// `measured` says how many bytes lie below it — so nothing is buffered, and a -/// walk that finds the ring moved since it was measured leaves a gap -/// [`Self::close`] takes out rather than a line out of place. -pub struct Kept<'r, 'a> { - report: &'r mut Report<'a>, - measured: Recovery, - /// Text offset of the records' region, past the head's reserve. - lo: usize, - /// What the region holds. - room: usize, - /// Measured bytes not yet walked past: the offset in the region the record - /// being looked at ends at, were the section whole. - left: usize, - /// The region's written span, or an empty one at zero. - placed: (usize, usize), - kept: u64, -} - -impl<'a> Report<'a> { - /// Open the recovery section at this report's end. - pub fn recovery<'r>(&'r mut self, measured: Recovery) -> Kept<'r, 'a> { - let lo = self.at.saturating_add(RECOVERY_HEAD_BYTES); - // Against `REPORT_BYTES` whatever this writer's own limit is: the - // account's reserve is the reset's, and a reopened report may spend it. - let room = REPORT_BYTES.saturating_sub(lo).min(RECOVERY_BYTES); - Kept { report: self, measured, lo, room, left: measured.whole.1, placed: (0, 0), kept: 0 } - } -} - -impl Kept<'_, '_> { - /// The stamp the walk starts from, or `None` where there is nothing to walk. - pub fn from(&self) -> Option { - self.measured.from - } - - /// One record of the walk. `false` ends it: the ring holds more from the - /// break on than was measured, and what lies below has nowhere to go. - pub fn put(&mut self, message: &str, line: &impl core::fmt::Display) -> bool { - if !usb_wrote(message) { - return true; - } - let Some(below) = self.left.checked_sub(recovery_line_bytes(line)) else { - return false; - }; - let ends_at = core::mem::replace(&mut self.left, below); - // Newer than the oldest that fit. - if ends_at > self.room { - return true; - } - let from = HEADER.saturating_add(self.lo); - let slot = self.report.page.get_mut(from.saturating_add(below)..from.saturating_add(ends_at)); - let Some(slot) = slot else { return true }; - let mut slot = Slot { out: slot, at: 0 }; - let _ = core::fmt::write(&mut slot, format_args!("{RECOVERY_OPENS_WITH}{line}\n")); - if self.kept == 0 { - self.placed.1 = ends_at; - } - self.placed.0 = below; - self.kept = self.kept.saturating_add(1); - true - } - - /// Write the line that counts the section above what was placed, and move - /// the records up against it. - pub fn close(self) { - let Self { report, measured, lo, placed, kept, .. } = self; - if measured.from.is_none() { - let _ = core::fmt::Write::write_fmt(report, format_args!("{RECOVERY_NONE}\n")); - return; - } - let _ = core::fmt::Write::write_fmt( - report, - format_args!( - "{RECOVERY_OPENS_WITH}{kept}{RECOVERY_KEPT_OF}{}{RECOVERY_COUNTED}\n", - measured.whole.0 - ), - ); - let from = HEADER.saturating_add(lo); - let (begins, ends) = (from.saturating_add(placed.0), from.saturating_add(placed.1)); - let to = HEADER.saturating_add(report.at); - // The head is no wider than its reserve, so the records only ever move - // down; a page that could not take the move keeps the head alone. - if begins < ends && to <= begins && ends <= HEADER.saturating_add(REPORT_BYTES) { - report.page.copy_within(begins..ends, to); - report.at = report.at.saturating_add(ends - begins); - } - } -} - -/// A `fmt::Write` over one placed line, which drops what its slot cannot hold. -struct Slot<'s> { - out: &'s mut [u8], - at: usize, -} - -impl core::fmt::Write for Slot<'_> { - fn write_str(&mut self, s: &str) -> core::fmt::Result { - let end = self.at.saturating_add(s.len()); - if let Some(slot) = self.out.get_mut(self.at..end) { - slot.copy_from_slice(s.as_bytes()); - self.at = end; - } - Ok(()) - } -} /// The stamp a page carries, read without checking anything. /// @@ -1369,217 +1157,6 @@ mod tests { /// ARMED carries no text at all, and that is not the same as no page. - /// A log ring as the section's walks see it: `(stamp, message)`, and the - /// order is the reader's — newest first. - type Ring = std::vec::Vec<(u64, std::string::String)>; - - fn rendered(at_ns: u64, message: &str) -> std::string::String { - format!("[{}.{:03} cpu5] {message}", at_ns / 1_000, at_ns % 1_000) - } - - /// Both walks over `measured_on` and `placed_from`, which differ only where - /// a test moves the ring between them. - fn section(report: &mut Report<'_>, measured_on: &Ring, placed_from: &Ring) { - let mut measured = Recovery::new(); - for (at_ns, message) in measured_on { - measured.saw(*at_ns, message, &rendered(*at_ns, message)); - } - let mut kept = report.recovery(measured); - if let Some(from) = kept.from() { - for (at_ns, message) in placed_from.iter().filter(|(at_ns, _)| *at_ns >= from) { - if !kept.put(message, &rendered(*at_ns, message)) { - break; - } - } - } - kept.close(); - } - - fn newest_first(oldest_first: &[(u64, &str)]) -> Ring { - oldest_first.iter().rev().map(|(at, message)| (*at, (*message).into())).collect() - } - - fn sealed_text(page: &[u8; BYTES]) -> std::string::String { - let (_, _, _, text) = recover(page).expect("a sealed report"); - std::str::from_utf8(text).expect("text").into() - } - - /// The boot this exists for: enumeration, then a break and its recovery, - /// then a minute of somebody else's records. - const A_BREAK_AND_ITS_RECOVERY: &[(u64, &str)] = &[ - (595, "xHCI: port 13 connected"), - (596, "usb-storage: disk 0 ready on slot 5, 7507812 blocks of 512 B"), - (3_412, "usb-storage: 00:14.0 slot 5 transport broke on SCSI 0x2a: a staged break; break 1 of 3 running"), - (3_412, "xHCI: 00:14.0 slot 5 endpoint 3 is Running, recovering"), - (3_413, "sched: cpu=4 ready=0 dying=0 parked=1"), - (3_420, "usb-storage: 00:14.0 slot 5 Reset Recovery took"), - (3_421, "usb-storage: 00:14.0 slot 5 SCSI 0x2a completed after 1 break(s) running"), - (11_182, "PMM: 62/16038MB used"), - ]; - - #[test] - fn the_section_is_the_usb_stacks_records_from_the_first_break_on_oldest_first() { - let ring = newest_first(A_BREAK_AND_ITS_RECOVERY); - let mut page = blank(); - let mut report = Report::new(&mut page); - section(&mut report, &ring, &ring); - report.seal(State::Wedged, STAMP, STICK); - let want = format!( - "{RECOVERY_OPENS_WITH}4 of the 4{RECOVERY_COUNTED}\n\ - {RECOVERY_OPENS_WITH}{}\n{RECOVERY_OPENS_WITH}{}\n{RECOVERY_OPENS_WITH}{}\n\ - {RECOVERY_OPENS_WITH}{}\n", - rendered(3_412, A_BREAK_AND_ITS_RECOVERY[2].1), - rendered(3_412, A_BREAK_AND_ITS_RECOVERY[3].1), - rendered(3_420, A_BREAK_AND_ITS_RECOVERY[5].1), - rendered(3_421, A_BREAK_AND_ITS_RECOVERY[6].1), - ); - assert_eq!(sealed_text(&page), want); - } - - /// Both of the driver's spellings open a section, and nothing else does: a - /// disk that gave up says `broke 3 times running`, after a break that - /// already opened it. - #[test] - fn a_break_is_told_by_what_the_driver_says_and_by_who_said_it() { - assert!(says_a_break("usb-storage: 00:14.0 slot 5 transport broke on SCSI 0x28: status")); - assert!(says_a_break("usb-storage: slot 5 broke on TEST UNIT READY: no answer")); - assert!(!says_a_break("usb-storage: 00:14.0 slot 5 SCSI 0x2a broke 3 times running")); - assert!(!says_a_break("root: the transport broke on SCSI 0x28")); - assert!(usb_wrote("xHCI: Stop Endpoint timed out after 2000 ms")); - assert!(!usb_wrote("usb-quiesce: xHCI 00:14.0 halted=true")); - assert!(!usb_wrote("[ 3.412 cpu5 kernel] xHCI: nested")); - } - - /// Nothing links this crate to the driver whose records it picks out, so - /// the two spellings are held to the driver's own source. - #[test] - fn the_driver_says_a_break_the_way_the_section_finds_one() { - let at = concat!(env!("CARGO_MANIFEST_DIR"), "/../../kernel/src/drivers/xhci/wait/msc.rs"); - let source = std::fs::read_to_string(at).expect("the mass-storage driver"); - for said in [ - "log!(\"usb-storage: {slot} transport broke on SCSI {opcode:#04x}: {broke}; \\", - "log!(\"usb-storage: slot {} broke on TEST UNIT READY: {broke}\"", - ] { - assert!(source.contains(said), "{at} does not write {said:?}"); - let message = said.trim_start_matches("log!(\""); - assert!(says_a_break(message), "{message:?} opens no section"); - } - } - - #[test] - fn a_boot_with_no_break_says_so_in_one_line() { - let ring = newest_first(&A_BREAK_AND_ITS_RECOVERY[..2]); - let mut page = blank(); - let mut report = Report::new(&mut page); - section(&mut report, &ring, &ring); - report.seal(State::Done, STAMP, STICK); - assert_eq!(sealed_text(&page), format!("{RECOVERY_NONE}\n")); - } - - /// A transport that storms: far more than the section holds. The break that - /// opened it and the records after it are kept, the newest are counted, and - /// the tail and the account under the section still have their room. - #[test] - fn a_storm_keeps_the_first_break_and_counts_what_it_dropped() { - let mut storm: std::vec::Vec<(u64, std::string::String)> = std::vec::Vec::new(); - for n in 0..400u64 { - let code = if n % 2 == 0 { 3 } else { 4 }; - storm.push(( - 3_471 + n, - format!("usb-storage: 00:14.0 slot 5 transport broke on SCSI 0x28: phase completion code {code}, n={n}"), - )); - } - let ring: Ring = storm.iter().rev().cloned().collect(); - let mut page = blank(); - let mut report = Report::new(&mut page); - report.write(b"the boot deadline expired\n"); - section(&mut report, &ring, &ring); - let after_the_section = report.at; - report.tail(b"[120.000 cpu4 kernel] the newest record\n", RECORD_OPENS_WITH); - report.seal(State::Wedged, STAMP, STICK); - - let text = sealed_text(&page); - let lines: std::vec::Vec<&str> = text.lines().collect(); - assert_eq!(lines[0], "the boot deadline expired"); - let head = lines[1].strip_prefix(RECOVERY_OPENS_WITH).expect("the section's head"); - let kept: usize = head.split(' ').next().expect("a count").parse().expect("a number"); - assert!(head.starts_with(&format!("{kept} of the 400 ")), "{head}"); - assert!(kept > 16 && kept < 400, "kept {kept}"); - // The oldest, in order, every one whole. - for (n, line) in lines[2..2 + kept].iter().enumerate() { - assert_eq!(*line, format!("{RECOVERY_OPENS_WITH}{}", rendered(storm[n].0, &storm[n].1))); - } - assert_eq!(lines[2 + kept], "[120.000 cpu4 kernel] the newest record"); - assert!( - after_the_section <= "the boot deadline expired\n".len() + RECOVERY_HEAD_BYTES + RECOVERY_BYTES, - "the section ran to {after_the_section}" - ); - assert!(text.len() <= REPORT_BYTES); - } - - /// The ring is live under both walks: a record the first walk measured can - /// be gone by the second, and one it never saw can be there. Either way - /// every line on the page is a whole one, in order, under a head that - /// counts what is there. - #[test] - fn a_ring_that_moved_between_the_walks_leaves_no_line_out_of_place() { - let whole = newest_first(A_BREAK_AND_ITS_RECOVERY); - let recycled: Ring = - whole.iter().filter(|(_, m)| !m.contains("Reset Recovery took")).cloned().collect(); - let mut later = whole.clone(); - later.insert(0, (3_500, "xHCI: 00:14.0 slot 5 endpoint 4 is Halted, recovering".into())); - - for placed_from in [&recycled, &later] { - let mut page = blank(); - let mut report = Report::new(&mut page); - section(&mut report, &whole, placed_from); - report.write(b"under the section\n"); - report.seal(State::Wedged, STAMP, STICK); - let text = sealed_text(&page); - let lines: std::vec::Vec<&str> = text.lines().collect(); - let head = lines[0].strip_prefix(RECOVERY_OPENS_WITH).expect("the section's head"); - let kept: usize = head.split(' ').next().expect("a count").parse().expect("a number"); - assert!(kept >= 3, "{text}"); - assert_eq!(lines.len(), 1 + kept + 1, "{text}"); - // Each a whole record of the ring it was placed from, oldest first. - let mut at = placed_from.len(); - for line in &lines[1..=kept] { - let found = placed_from[..at] - .iter() - .rposition(|(stamp, m)| **line == format!("{RECOVERY_OPENS_WITH}{}", rendered(*stamp, m))); - at = found.unwrap_or_else(|| panic!("{line:?} is no record, or is out of order\n{text}")); - } - assert_eq!(lines[1 + kept], "under the section"); - } - } - - /// Under a sealed report, where the reset's own account follows it: the - /// section is covered by the envelope as one piece and never spends the - /// account's reserve on records. - #[test] - fn the_section_under_a_sealed_report_leaves_the_account_its_reserve() { - let ring = newest_first(A_BREAK_AND_ITS_RECOVERY); - let mut page = blank(); - seal(&mut page, State::Done, STAMP, STICK, &[b'x'; REPORT_BYTES - 64]); - let mut flushed = 0; - let mut account = Account::new( - Report::reopened(&mut page, REPORT_BYTES - 64), - State::Done, - STAMP, - STICK, - || flushed += 1, - ); - account.block(|report| section(report, &ring, &ring)); - let _ = writeln!(account, "usb-quiesce: the account"); - assert_eq!(flushed, 2); - let text = sealed_text(&page); - let under = &text[REPORT_BYTES - 64..]; - assert_eq!( - under, - format!("{RECOVERY_OPENS_WITH}0 of the 4{RECOVERY_COUNTED}\nusb-quiesce: the account\n") - ); - } - #[test] fn an_armed_page_with_nothing_in_it_is_still_a_state() { let mut page = blank(); diff --git a/lib/blockserve/src/lib.rs b/lib/blockserve/src/lib.rs index afc2ecf6158..401ee903359 100644 --- a/lib/blockserve/src/lib.rs +++ b/lib/blockserve/src/lib.rs @@ -17,7 +17,5 @@ pub use table::Partition; /// A partition GUID as the lines say it. pub fn guid_text(bytes: [u8; 16]) -> String { - use toyos_abi::part::{PartGuid, GUID_TEXT_LEN}; - let mut buf = [0u8; GUID_TEXT_LEN]; - PartGuid(bytes).write_text(&mut buf).to_string() + toyos_gpt::Guid(bytes).to_string() } diff --git a/lib/blockserve/src/link.rs b/lib/blockserve/src/link.rs index 9a581b166d5..4f091143fa7 100644 --- a/lib/blockserve/src/link.rs +++ b/lib/blockserve/src/link.rs @@ -39,11 +39,16 @@ pub struct Link { } impl Link { - /// The link of the driver `label` names. - pub fn new(label: String) -> Self { + /// The link of the driver `label` names; `None` for a process its + /// manifest gives no `disk`. + pub fn new(label: String) -> Option { + let first = match endow::service(disk::PORT) { + Err(EndowError::NotEndowed) => return None, + first => first, + }; let mut link = Self { label, conn: None, rx: FrameRx::new(), offers: BTreeMap::new(), settled: None }; - link.connect(); - link + link.connect_from(first); + Some(link) } /// Connect, and say everything again; a fresh connection that will not @@ -51,9 +56,15 @@ impl Link { /// connection's frames on any of them is a refusal that does not change, /// and the driver ends by name. fn connect(&mut self) { + self.connect_from(endow::service(disk::PORT)); + } + + /// [`Self::connect`], its first connection's answer already in hand. + fn connect_from(&mut self, first: Result) { + let mut first = Some(first); for _ in 0..CONNECTS { self.rx = FrameRx::new(); - self.conn = match endow::service(disk::PORT) { + self.conn = match first.take().unwrap_or_else(|| endow::service(disk::PORT)) { Ok(conn) => Some(conn), Err(EndowError::ServerGone) => { println!("{}: the block service's port is closed for good; nothing new is served", self.label); diff --git a/lib/blockserve/src/sessions.rs b/lib/blockserve/src/sessions.rs index 69ba7551948..820801d14e9 100644 --- a/lib/blockserve/src/sessions.rs +++ b/lib/blockserve/src/sessions.rs @@ -327,6 +327,17 @@ impl Sessions { answered } + /// The disk left: every request on it is answered not done, every + /// client told, and every session ended. + pub fn end(&mut self, dev: &mut impl Device) { + self.reset(); + for s in self.sessions.values_mut() { + s.closing = true; + } + self.publish(); + self.retire(dev); + } + pub fn losses(&self) -> u64 { self.losses } diff --git a/lib/blockserve/src/table.rs b/lib/blockserve/src/table.rs index 9effecbbb15..7d9d36afd21 100644 --- a/lib/blockserve/src/table.rs +++ b/lib/blockserve/src/table.rs @@ -18,13 +18,12 @@ pub struct Partition { /// Every partition the table on `sectors` names, each checked past /// `toyos_gpt::locate`'s range and overlap checks and held to whole blocks; -/// none for a disk whose table does not read, said by `name`. -pub fn read(name: &str, sectors: &mut impl toyos_gpt::Sectors) -> Vec { +/// or why the disk carries no table this reads. Pure: [`say`] says it. +pub fn read(sectors: &mut dyn toyos_gpt::Sectors) -> Result, String> { let per = BLOCK_BYTES as u64 / u64::from(sectors.lba_bytes()); let mut found = [None; 128]; if let Err(why) = toyos_gpt::list(sectors, &mut found) { - println!("{name}: the disk carries no partition table this driver reads: {why:?}"); - return Vec::new(); + return Err(format!("{why:?}")); } let mut parts = Vec::new(); for entry in found.iter().flatten() { @@ -50,6 +49,19 @@ pub fn read(name: &str, sectors: &mut impl toyos_gpt::Sectors) -> Vec }; parts.push(Partition { unique: listed.unique_guid().0, kind: listed.type_guid().0, span }); } + Ok(parts) +} + +/// Say what [`read`] found, as `name`, and answer the partitions: none for a +/// disk with no table. +pub fn say(name: &str, read: Result, String>) -> Vec { + let parts = match read { + Ok(parts) => parts, + Err(why) => { + println!("{name}: the disk carries no partition table this driver reads: {why}"); + return Vec::new(); + } + }; for part in &parts { let guid = crate::guid_text(part.unique); match &part.span { diff --git a/lib/fat32/check/src/lib.rs b/lib/fat32/check/src/lib.rs index 817ab054d60..52ea3c02885 100644 --- a/lib/fat32/check/src/lib.rs +++ b/lib/fat32/check/src/lib.rs @@ -167,6 +167,37 @@ pub enum Complaint { More { dropped: usize }, } +/// What of `complaints` no stop mid-call can explain, on a volume written by a +/// writer that orders its writes as `toyos-fat32` does (`toyos_fat32`'s +/// `repair` module names the windows): +/// +/// - **clusters claimed before anything reaches them** (a lost chain); +/// - **one file's chain grown before its size is** (too long for its size): +/// a claim is end-of-chain before it is linked and the size is written +/// last, so every byte the size names is held. One call grows one file, so +/// a second such file is not explained; +/// - **one FAT entry's copies disagreeing**, written one copy at a time; +/// - **the FSInfo free count off**: fatgen103 §5 calls `FSI_Free_Count` "not +/// necessarily correct" and has a driver that uses it range-check it, so it +/// is a hint and never the volume. +/// +/// A chain too short for its size, a cross-link, a cycle or a broken entry is +/// never explained: no stop leaves a size naming bytes no cluster holds. +pub fn unexplained_by_a_stop(complaints: &[Complaint]) -> Vec<&Complaint> { + let mut grown = 0; + complaints + .iter() + .filter(|c| match c { + Complaint::LostChain { .. } | Complaint::FatMirror { .. } | Complaint::FsInfoFreeCount { .. } => false, + Complaint::ChainTooLong { .. } => { + grown += 1; + grown > 1 + } + _ => true, + }) + .collect() +} + /// The complaints one per line, for a caller that is about to fail a test. pub fn describe(complaints: &[Complaint]) -> String { let mut out = String::new(); @@ -544,3 +575,22 @@ impl fmt::Display for Complaint { } } } + +#[cfg(test)] +mod stop_tests { + use super::*; + + /// What the T14's first unclean reset mid-append left (#971, r5): a chain + /// two clusters past its file's size and FSInfo two clusters behind. A stop + /// explains those, one file's growth only, and never a short chain. + #[test] + fn a_stop_explains_a_grown_chain_and_a_stale_free_count_and_nothing_worse() { + let grown = |path: &str| Complaint::ChainTooLong { path: path.into(), size: 200_402, held: 394, needed: 392 }; + let free = Complaint::FsInfoFreeCount { declared: 53_763, counted: 53_761 }; + assert!(unexplained_by_a_stop(&[grown("/a_0008.log"), free.clone()]).is_empty()); + assert!(unexplained_by_a_stop(&[Complaint::LostChain { first: 9, clusters: 2 }]).is_empty()); + assert_eq!(unexplained_by_a_stop(&[grown("/a.log"), grown("/b.log")]).len(), 1, "two files grown by one stop"); + let short = Complaint::ChainTooShort { path: "/a.log".into(), size: 200_402, held: 390, needed: 392 }; + assert_eq!(unexplained_by_a_stop(&[short, free]).len(), 1, "a size naming bytes no cluster holds"); + } +} diff --git a/lib/gpt/src/guid.rs b/lib/gpt/src/guid.rs index dec1c9895e3..5c320d51883 100644 --- a/lib/gpt/src/guid.rs +++ b/lib/gpt/src/guid.rs @@ -91,6 +91,25 @@ impl Guid { pub const fn is_zero(&self) -> bool { u128::from_ne_bytes(self.0) == 0 } + + /// The registry text form [`Display`](fmt::Display) writes — uppercase + /// hex, the first three groups little-endian on the disk (UEFI 2.10 + /// Appendix A) — exactly, and nothing else: one spelling per GUID. + pub fn parse(text: &str) -> Option { + const TEXT_ORDER: [usize; 16] = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15]; + let text = text.as_bytes(); + if text.len() != 36 || [8, 13, 18, 23].iter().any(|&at| text.get(at) != Some(&b'-')) { + return None; + } + let hex = |byte: u8| char::from(byte).to_digit(16).filter(|_| !byte.is_ascii_lowercase()); + let mut digits = text.iter().filter(|&&b| b != b'-').map(|&b| hex(b)); + let mut bytes = [0u8; 16]; + for &at in &TEXT_ORDER { + let (high, low) = (digits.next()??, digits.next()??); + *bytes.get_mut(at)? = u8::try_from(high << 4 | low).ok()?; + } + digits.next().is_none().then_some(Self(bytes)) + } } impl fmt::Display for Guid { @@ -131,6 +150,32 @@ mod tests { assert_eq!(&text[..], b"C12A7328-F81F-11D2-BA4B-00A0C93EC93B"); } + #[test] + fn the_registry_text_parses_to_the_bytes_it_prints_from() { + let text = heapless_format(Guid::EFI_SYSTEM); + let text = core::str::from_utf8(&text).expect("ASCII"); + assert_eq!(Guid::parse(text), Some(Guid::EFI_SYSTEM)); + } + + /// One spelling per GUID: every near miss a config could plausibly write + /// is refused, not normalised. + #[test] + fn a_guid_is_the_uppercase_registry_form_and_nothing_else() { + for bad in [ + "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "{C12A7328-F81F-11D2-BA4B-00A0C93EC93B}", + "C12A7328F81F11D2BA4B00A0C93EC93B", + "C12A7328-F81F-11D2-BA4B-00A0C93EC93", + "C12A7328-F81F-11D2-BA4B-00A0C93EC93BB", + "C12A7328-F81F-11D2-BA4B00-A0C93EC93B", + "G12A7328-F81F-11D2-BA4B-00A0C93EC93B", + "C12A7328-F81F-11D2-BA4B-00A0C93EC9\u{e9}", + "", + ] { + assert_eq!(Guid::parse(bad), None, "{bad:?} parsed"); + } + } + #[test] fn zero_is_zero() { assert!(Guid::ZERO.is_zero()); diff --git a/lib/input/Cargo.toml b/lib/input/Cargo.toml index 2fb9233a6f2..b1675e8e6a2 100644 --- a/lib/input/Cargo.toml +++ b/lib/input/Cargo.toml @@ -1,6 +1,6 @@ -# 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. +# A member of the host workspace (root `Cargo.toml`): usbd links its `Keys` +# through toyos-usbhid, the owners of input link the rest, and its tests run on +# the host. [package] name = "toyos-input" diff --git a/lib/input/src/keys.rs b/lib/input/src/keys.rs index b6fc783e343..b6eb0422ac0 100644 --- a/lib/input/src/keys.rs +++ b/lib/input/src/keys.rs @@ -22,12 +22,6 @@ 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 @@ -47,14 +41,19 @@ impl Usages { 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])) + /// The usages in exactly one of `a` and `b`. + pub fn symmetric(a: Self, b: Self) -> Self { + Self(core::array::from_fn(|w| a.0[w] ^ b.0[w])) + } + + /// How many usages are held. + pub fn count(self) -> u32 { + self.0.iter().map(|w| w.count_ones()).sum() } - /// Every one of these but the modifiers. - pub fn keys(self) -> Self { - Self(core::array::from_fn(|w| self.0[w] & !MODIFIERS.0[w])) + /// These, but none of `other`. + pub fn without(self, other: Self) -> Self { + Self(core::array::from_fn(|w| self.0[w] & !other.0[w])) } } diff --git a/lib/input/src/lib.rs b/lib/input/src/lib.rs index a32397b6dee..1bc0e73ff3a 100644 --- a/lib/input/src/lib.rs +++ b/lib/input/src/lib.rs @@ -8,10 +8,10 @@ //! it; a device's `gone` releases what it held, a connection's end what //! every device on it held, and neither anything 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. +//! [`wire`] is the one reader of a source's records off the `input` port: +//! what does not decode is refused by name. [`keys`] is one keyboard's held +//! set, which a source's HID decode holds too. Nothing here touches a device, +//! allocates or holds a lock. #![no_std] #![forbid(unsafe_code)] diff --git a/lib/input/src/merge.rs b/lib/input/src/merge.rs index 0615ffd9af6..b1cc5727f74 100644 --- a/lib/input/src/merge.rs +++ b/lib/input/src/merge.rs @@ -6,21 +6,17 @@ use toyos_abi::input::{RawKeyEvent, MOD_ALT, MOD_CTRL, MOD_GUI, MOD_RELEASED, MO use crate::keys::{Keys, Transitions, Usages}; use crate::wire::{Motion, Report, CONNECTIONS, DEVICES}; -/// 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; +/// The sources the owner merges: one per connection on the port. +pub const SLOTS: usize = CONNECTIONS; /// 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)) + (n < CONNECTIONS).then_some(Self(n)) } } diff --git a/lib/input/src/wire.rs b/lib/input/src/wire.rs index ca2568ea073..3ee135a5045 100644 --- a/lib/input/src/wire.rs +++ b/lib/input/src/wire.rs @@ -1,5 +1,5 @@ -//! The `input` port's frames, and the records the kernel's streams carry, -//! decoded by one reader whichever source sent them. +//! The `input` port's frames, decoded by one reader whichever source sent +//! them. //! //! A sender numbers its devices `source`s: a source names one device for the //! life of its connection, so a late record never reaches a later device. A @@ -9,14 +9,14 @@ //! 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}; +use toyos_abi::input::{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 rows that may receive [`PORT`]: a connector is the authority to type +/// into the focused window, and these are the programs that drive keyboards. +pub const HOLDERS: &[&str] = &["ps2server", "usbd"]; /// The most connections the owner serves at once; one past it is refused. pub const CONNECTIONS: usize = 4; @@ -44,11 +44,6 @@ pub const MAX_PAYLOAD: usize = POINTER_LEN; /// than this. pub const DEVICES: usize = 16; -/// A key record's bytes on the kernel's keyboard stream: one [`RawKeyEvent`]. -pub const STREAM_KEY_LEN: usize = core::mem::size_of::(); -/// A pointer record's bytes on the kernel's mouse stream: one [`MouseEvent`]. -pub const STREAM_POINTER_LEN: usize = core::mem::size_of::(); - /// The buttons boot-mouse order names. pub const BUTTONS: u8 = 0x1F; @@ -135,7 +130,7 @@ pub fn decode(msg_type: u32, payload: &[u8]) -> Result { match msg_type { MSG_KEY => { let [s0, s1, keycode, modifiers]: [u8; KEY_LEN] = payload.try_into().map_err(|_| length())?; - Ok(Record::Key { source: u16::from_le_bytes([s0, s1]), key: key(&[keycode, modifiers])? }) + Ok(Record::Key { source: u16::from_le_bytes([s0, s1]), key: key(keycode, modifiers)? }) } MSG_POINTER => { let [s0, s1, buttons, scroll, kind, x0, x1, y0, y1]: [u8; POINTER_LEN] = @@ -152,10 +147,8 @@ pub fn decode(msg_type: u32, payload: &[u8]) -> Result { } } -/// One key's usage and transition, as a source's frame or the kernel's -/// keyboard stream carries it. -pub fn key(bytes: &[u8; STREAM_KEY_LEN]) -> Result { - let [keycode, modifiers] = *bytes; +/// One key's usage and transition. +fn key(keycode: u8, modifiers: u8) -> Result { if keycode == 0 { return Err(Refused::NoUsage); } @@ -165,13 +158,6 @@ pub fn key(bytes: &[u8; STREAM_KEY_LEN]) -> Result { Ok(RawKeyEvent { keycode, modifiers }) } -/// One pointer report, as the kernel's mouse stream carries it. -pub fn pointer(bytes: &[u8; STREAM_POINTER_LEN]) -> Result { - let [source, buttons, scroll, kind, x0, x1, y0, y1] = *bytes; - let motion = motion(kind, i16::from_ne_bytes([x0, x1]), i16::from_ne_bytes([y0, y1]))?; - believed(Report { source: u16::from(source), buttons, scroll: scroll as i8, motion }) -} - fn motion(kind: u8, x: i16, y: i16) -> Result { let on_surface = |v: i16| (0..=POINTER_ABSOLUTE_MAX).contains(&v); match kind { diff --git a/lib/input/tests/input.rs b/lib/input/tests/input.rs index 28ec20d7b56..8c2581cbbe1 100644 --- a/lib/input/tests/input.rs +++ b/lib/input/tests/input.rs @@ -1,9 +1,8 @@ //! 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_abi::input::{RawKeyEvent, MOD_RELEASED, MOD_SHIFT, POINTER_ABSOLUTE, POINTER_RELATIVE}; use toyos_input::wire::{ self, Motion, Record, Refused, Report, DEVICES, GONE_LEN, KEY_LEN, MAX_PAYLOAD, MSG_GONE, MSG_KEY, MSG_POINTER, POINTER_LEN, - STREAM_POINTER_LEN, }; use toyos_input::{Full, Merge, Pointer, Slot, State}; @@ -22,8 +21,21 @@ fn slot(n: usize) -> Slot { Slot::connection(n).expect("a connection the port holds") } -fn bytes(event: &MouseEvent) -> [u8; STREAM_POINTER_LEN] { - toyos_abi::usersafe::bytes(event).try_into().expect("a record's length") +/// A key frame of source 0. +fn key(keycode: u8, modifiers: u8) -> Result { + match wire::decode(MSG_KEY, &[0, 0, keycode, modifiers])? { + Record::Key { key, .. } => Ok(key), + other => panic!("a key frame decoded as {other:?}"), + } +} + +/// A pointer frame of source 0. +fn pointer(buttons: u8, kind: u8, x: i16, y: i16) -> Result { + let ([x0, x1], [y0, y1]) = (x.to_le_bytes(), y.to_le_bytes()); + match wire::decode(MSG_POINTER, &[0, 0, buttons, 0, kind, x0, x1, y0, y1])? { + Record::Pointer(report) => Ok(report), + other => panic!("a pointer frame decoded as {other:?}"), + } } fn relative(source: u16, buttons: u8, dx: i16, dy: i16) -> Report { @@ -32,38 +44,31 @@ fn relative(source: u16, buttons: u8, dx: i16, dy: i16) -> Report { #[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)); + assert_eq!(key(0, 0), Err(Refused::NoUsage)); + assert_eq!(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!(key(A, bit), Err(Refused::Modifiers(bit)), "modifier bit {bit:#04x}"); + assert_eq!(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))); + assert_eq!(key(A, 0), Ok(press(A))); + assert_eq!(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_eq!(pointer(buttons, POINTER_RELATIVE, 0, 0), Err(Refused::Buttons(buttons))); } - assert!(wire::pointer(&bytes(&event(0x1F, POINTER_RELATIVE, 0, 0))).is_ok(), "five buttons"); + assert!(pointer(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))); + assert_eq!(pointer(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!(pointer(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 }) - ); + assert_eq!(pointer(1, POINTER_ABSOLUTE, 32767, 0).map(|r| r.motion), Ok(Motion::Absolute { x: 32767, y: 0 })); + assert_eq!(pointer(1, POINTER_RELATIVE, -300, 255).map(|r| r.motion), Ok(Motion::Relative { dx: -300, dy: 255 })); } #[test] @@ -81,20 +86,6 @@ fn a_frame_of_another_type_or_length_is_refused() { } } -/// A frame's key and pointer fields are refused as the kernel's streams' are. -#[test] -fn a_frames_key_and_pointer_are_held_to_the_same_rules() { - assert_eq!(wire::decode(MSG_KEY, &[1, 0, 0, 0]), Err(Refused::NoUsage)); - assert_eq!(wire::decode(MSG_KEY, &[1, 0, A, MOD_SHIFT]), Err(Refused::Modifiers(MOD_SHIFT))); - let pointer = |buttons, kind, x: i16| { - let [x0, x1] = x.to_le_bytes(); - wire::decode(MSG_POINTER, &[0, 0, buttons, 0, kind, x0, x1, 0, 0]) - }; - assert_eq!(pointer(1 << 5, POINTER_RELATIVE, 0), Err(Refused::Buttons(1 << 5))); - assert_eq!(pointer(0, 2, 0), Err(Refused::Kind(2))); - assert_eq!(pointer(0, POINTER_ABSOLUTE, -1), Err(Refused::Position { x: -1, y: 0 })); -} - #[test] fn every_record_decodes_as_the_frame_it_was_sent_as() { let records = [ @@ -116,9 +107,9 @@ fn every_record_decodes_as_the_frame_it_was_sent_as() { 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), 0, press(SHIFT)).unwrap().collect::>(), [RawKeyEvent { keycode: SHIFT, modifiers: MOD_SHIFT }]); - assert_eq!(merge.key(Slot::KERNEL, 0, press(A)).unwrap().collect::>(), [RawKeyEvent { keycode: A, modifiers: MOD_SHIFT }]); + assert_eq!(merge.key(slot(3), 0, press(A)).unwrap().collect::>(), [RawKeyEvent { keycode: A, modifiers: MOD_SHIFT }]); assert_eq!(merge.key(slot(0), 0, lift(SHIFT)).unwrap().collect::>(), [RawKeyEvent { keycode: SHIFT, modifiers: MOD_RELEASED }]); - assert_eq!(merge.key(Slot::KERNEL, 0, lift(A)).unwrap().collect::>(), [RawKeyEvent { keycode: A, modifiers: MOD_RELEASED }]); + assert_eq!(merge.key(slot(3), 0, lift(A)).unwrap().collect::>(), [RawKeyEvent { keycode: A, modifiers: MOD_RELEASED }]); } #[test] @@ -147,9 +138,9 @@ fn a_source_that_ends_releases_what_it_held_and_nothing_another_holds() { 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)).unwrap().buttons, 1); - assert_eq!(pointer.report(Slot::KERNEL, relative(4, 2, 0, 0)).unwrap().buttons, 3, "another slot's pointer"); - assert_eq!(pointer.report(Slot::KERNEL, relative(5, 0, 0, 0)).unwrap().buttons, 3, "a third pointer, holding nothing"); - assert_eq!(pointer.report(Slot::KERNEL, relative(4, 0, 0, 0)).unwrap().buttons, 1); + assert_eq!(pointer.report(slot(3), relative(4, 2, 0, 0)).unwrap().buttons, 3, "another slot's pointer"); + assert_eq!(pointer.report(slot(3), relative(5, 0, 0, 0)).unwrap().buttons, 3, "a third pointer, holding nothing"); + assert_eq!(pointer.report(slot(3), relative(4, 0, 0, 0)).unwrap().buttons, 1); assert_eq!(pointer.report(slot(0), relative(1, 4, 0, 0)).unwrap().buttons, 5, "one slot's two pointers"); } diff --git a/lib/inspect/Cargo.toml b/lib/inspect/Cargo.toml index 8c387ea5f60..16da3ede483 100644 --- a/lib/inspect/Cargo.toml +++ b/lib/inspect/Cargo.toml @@ -8,3 +8,5 @@ license = "MIT OR Apache-2.0" [dependencies] # The inventory records `dev` renders, whose layout the ABI owns. toyos-abi = { path = "../../sdk/abi" } +# The registry text a loaded partition's GUID renders as. +toyos-gpt = { path = "../gpt" } diff --git a/lib/inspect/src/dev.rs b/lib/inspect/src/dev.rs index ce59d5b671c..7c104c7816a 100644 --- a/lib/inspect/src/dev.rs +++ b/lib/inspect/src/dev.rs @@ -7,9 +7,7 @@ //! ```text //! dev.cpus, dev.memory.{total,used}_bytes //! dev.pci..{vendor,device,class,driver} -//! dev.usb..port.{function,speed,vendor,product} -//! dev.disk..blocks -//! dev.disk..part.{type,unique,first_lba,lbas,state} +//! dev.loaded. //! dev.class. //! ... and under a claimed device, holder. = //! ``` @@ -19,8 +17,7 @@ //! segment group, bus, device and function — and never contains a `.`. //! //! A PCI function's `driver` is `kernel` for one a kernel driver took, `none` -//! for one nobody drives, and `claimed` for one a process's claim holds; a -//! partition's `state` is `kernel`, `free` or `claimed` the same way. A claim +//! for one nobody drives, and `claimed` for one a process's claim holds. A claim //! whose handle the kernel found in a table adds `holder.` under what it //! holds; one moving between two tables adds nothing. //! @@ -33,10 +30,7 @@ use alloc::collections::BTreeSet; use alloc::format; use alloc::string::{String, ToString}; -use toyos_abi::inventory::{ - Claimed, Driven, Holder, PartState, PciAddr, Record, Role, UsbFunction, UsbSpeed, -}; -use toyos_abi::part::{PartGuid, GUID_TEXT_LEN}; +use toyos_abi::inventory::{Claimed, Driven, Holder, PciAddr, Record, Role}; use toyos_abi::syscall::SYSINFO_HEADER_SIZE; use crate::wire::Value; @@ -75,8 +69,7 @@ fn addr(at: PciAddr) -> String { } fn guid(bytes: [u8; 16]) -> String { - let mut buf = [0u8; GUID_TEXT_LEN]; - PartGuid(bytes).write_text(&mut buf).to_ascii_lowercase() + toyos_gpt::Guid(bytes).to_string().to_ascii_lowercase() } /// A holder's name as a value: its bytes up to the first NUL, or `pid` for @@ -88,18 +81,6 @@ fn name(holder: &Holder) -> String { } } -fn speed(s: UsbSpeed) -> &'static str { - match s { - UsbSpeed::Full => "full", - UsbSpeed::Low => "low", - UsbSpeed::High => "high", - UsbSpeed::Super => "super", - UsbSpeed::SuperPlusGen2x1 => "super-plus-gen2x1", - UsbSpeed::SuperPlusGen1x2 => "super-plus-gen1x2", - UsbSpeed::SuperPlusGen2x2 => "super-plus-gen2x2", - } -} - /// Paths under `dev`, each written once. struct Out(BTreeMap); @@ -121,8 +102,6 @@ pub fn render(machine: &Machine, records: &[Record]) -> Result = BTreeMap::new(); for record in records { match record { Record::Pci(p) => { @@ -140,35 +119,6 @@ pub fn render(machine: &Machine, records: &[Record]) -> Result { - let at = format!("usb.{}.port{}", addr(u.controller), u.port); - let function = match u.function { - UsbFunction::Keyboard => "keyboard", - UsbFunction::Pointer => "pointer", - UsbFunction::Storage => "storage", - }; - out.put(format!("{at}.function"), function.into())?; - out.put(format!("{at}.speed"), speed(u.speed).into())?; - out.put(format!("{at}.vendor"), format!("{:04x}", u.vendor).into())?; - out.put(format!("{at}.product"), format!("{:04x}", u.product).into())?; - } - Record::Block(b) => { - out.put(format!("disk.{}.blocks", b.device), b.blocks.into())?; - } - Record::Partition(p) => { - let at = format!("disk.{}.part{}", p.device, p.index); - out.put(format!("{at}.type"), guid(p.type_guid).into())?; - out.put(format!("{at}.unique"), guid(p.unique_guid).into())?; - out.put(format!("{at}.first_lba"), p.first_lba.into())?; - out.put(format!("{at}.lbas"), p.lbas.into())?; - let state = match p.state { - PartState::Free => "free", - PartState::Kernel => "kernel", - PartState::Claimed => "claimed", - }; - out.put(format!("{at}.state"), state.into())?; - parts.entry((p.device, p.unique_guid)).or_insert(at); - } Record::Claim(_) => {} Record::Loaded(l) => { let role = match l.role { @@ -189,12 +139,6 @@ pub fn render(machine: &Machine, records: &[Record]) -> Result format!("pci.{}", addr(at)), Claimed::Class(class) => format!("class.{}", class.class_name()), - // A claim on a partition no listed table carries is on nothing - // this layout has a path for. - Claimed::Partition { device, unique_guid } => match parts.get(&(device, unique_guid)) { - Some(at) => at.clone(), - None => continue, - }, }; if held.insert((at.clone(), c.holder.pid)) { out.put(format!("{at}.holder.{}", c.holder.pid), name(&c.holder).into())?; @@ -209,7 +153,7 @@ mod tests { use crate::path::check_path; use crate::Selector; use alloc::vec::Vec; - use toyos_abi::inventory::{Arrival, Block, Claim, Partition, Pci, Usb, NAME_BYTES}; + use toyos_abi::inventory::{Arrival, Claim, Loaded, Pci, NAME_BYTES}; use toyos_abi::syscall::DeviceType; fn holder(pid: u32, s: &str) -> Holder { @@ -226,47 +170,17 @@ mod tests { Record::Pci(Pci { arrival: Arrival(1), at, vendor: 0x8086, device: 0x15fc, class: 2, subclass: 0, prog_if: 0, driven }) } - fn part(device: u32, index: u32, unique: u8, state: PartState) -> Record { - Record::Partition(Partition { - device, - index, - type_guid: [0x28; 16], - unique_guid: [unique; 16], - first_lba: 2048, - lbas: 100, - state, - }) - } - fn records() -> Vec { let netstack = holder(9, "netstack"); alloc::vec![ pci(NIC, Driven::Claimed), pci(XHCI, Driven::Kernel), pci(PciAddr { segment: 1, bus: 0x80, dev: 5, func: 0 }, Driven::Claimed), - Record::Usb(Usb { - controller: XHCI, - port: 1, - speed: UsbSpeed::High, - vendor: 0x46f4, - product: 1, - function: UsbFunction::Storage, - }), - Record::Block(Block { device: 0, blocks: 4096 }), - Record::Block(Block { device: 1, blocks: 4096 }), - part(0, 0, 0xab, PartState::Kernel), - part(1, 0, 0xcd, PartState::Claimed), - part(1, 1, 0xef, PartState::Free), - // The image `dd`'d to a second disk: one GUID, two partitions. - part(0, 1, 0xcd, PartState::Free), + Record::Loaded(Loaded { role: Role::Log, unique_guid: [0xcd; 16] }), Record::Claim(Claim { on: Claimed::Pci(NIC), holder: netstack }), // The same claim through a second handle in the same table. Record::Claim(Claim { on: Claimed::Pci(NIC), holder: netstack }), Record::Claim(Claim { on: Claimed::Class(DeviceType::Framebuffer), holder: holder(7, "compositor") }), - Record::Claim(Claim { - on: Claimed::Partition { device: 1, unique_guid: [0xcd; 16] }, - holder: holder(12, "test-runner"), - }), ] } @@ -290,32 +204,15 @@ mod tests { // Claimed, and its handle was in no table the kernel walked. assert_eq!(text("dev.pci.0001:80:05:0.driver").as_deref(), Some("claimed")); assert!(!got.keys().any(|p| p.starts_with("dev.pci.0001:80:05:0.holder"))); - assert_eq!(text("dev.usb.0000:00:04:0.port1.function").as_deref(), Some("storage")); - assert_eq!(text("dev.usb.0000:00:04:0.port1.speed").as_deref(), Some("high")); - assert_eq!(text("dev.disk.1.blocks").as_deref(), Some("4096")); - assert_eq!(text("dev.disk.0.part0.state").as_deref(), Some("kernel")); - assert_eq!(text("dev.disk.1.part1.state").as_deref(), Some("free")); - // One GUID on two disks is two partitions, and the claim is on the - // one its device names. - assert_eq!(text("dev.disk.1.part0.state").as_deref(), Some("claimed")); - assert_eq!(text("dev.disk.1.part0.holder.12").as_deref(), Some("test-runner")); - assert_eq!(text("dev.disk.0.part1.state").as_deref(), Some("free")); - assert!(!got.keys().any(|p| p.starts_with("dev.disk.0.part1.holder"))); - assert_eq!( - text("dev.disk.0.part1.unique").as_deref(), - Some("cdcdcdcd-cdcd-cdcd-cdcd-cdcdcdcdcdcd") - ); + assert_eq!(text("dev.loaded.log").as_deref(), Some("cdcdcdcd-cdcd-cdcd-cdcd-cdcdcdcdcdcd")); assert_eq!(text("dev.class.framebuffer.holder.7").as_deref(), Some("compositor")); } #[test] fn a_path_two_records_render_to_is_refused_by_name() { let mut twice = records(); - twice.push(part(1, 1, 0x99, PartState::Kernel)); - assert_eq!( - render(&MACHINE, &twice), - Err(Repeated("dev.disk.1.part1.type".into())) - ); + twice.push(Record::Loaded(Loaded { role: Role::Log, unique_guid: [0x99; 16] })); + assert_eq!(render(&MACHINE, &twice), Err(Repeated("dev.loaded.log".into()))); let mut twice = records(); twice.push(pci(NIC, Driven::Free)); assert_eq!( @@ -324,17 +221,14 @@ mod tests { ); } - /// An address is one segment: `*.0.*` is the one disk numbered 0, and no - /// function 0, bus 0 or segment 0 of any other kind. + /// An address is one segment: `*.0.*` matches no function 0, bus 0 or + /// segment 0, which are parts of an address and never segments. #[test] fn a_star_never_lines_up_the_parts_of_an_address() { let got = render(&MACHINE, &records()).expect("no path twice"); let s = Selector::parse("*.0.*").unwrap(); let hits: Vec<&str> = got.keys().map(String::as_str).filter(|p| s.matches(p)).collect(); - assert!(!hits.is_empty()); - for path in &hits { - assert!(path.starts_with("dev.disk.0."), "{path}"); - } + assert!(hits.is_empty(), "{hits:?}"); let s = Selector::parse("dev.pci.*.driver").unwrap(); assert_eq!(got.keys().filter(|p| s.matches(p)).count(), 3); } diff --git a/lib/manifest/src/device.rs b/lib/manifest/src/device.rs index c4eb647e4c0..e91df86a396 100644 --- a/lib/manifest/src/device.rs +++ b/lib/manifest/src/device.rs @@ -1,10 +1,11 @@ //! What a `devices` or `drives` entry names. //! -//! A class, a partition or an ISA set is the ABI's own spelling +//! A class or an ISA set is the ABI's own spelling //! ([`DeviceRequest::parse`]). A PCI function is matched here, by its class -//! code and optionally its card: `pci:CC:SS:PP` or `pci:CC:SS:PP/VVVV:DDDD`, -//! lowercase hex, where `PP` may be `*` for every programming interface of the -//! subclass. **Every matcher carries its class**, so whether two entries +//! code and optionally its card and its place: `pci:CC:SS:PP`, then +//! `/VVVV:DDDD`, then `@BB:DD.F`, lowercase hex, where `PP` may be `*` for +//! every programming interface of the subclass. A place tells apart two +//! functions of one card, as two controllers on one bridge are. **Every matcher carries its class**, so whether two entries //! can name one function is decided from their text alone ([`Entry::overlaps`]), //! and a claim names the function a matcher found by its arrival. @@ -16,7 +17,7 @@ use toyos_abi::syscall::{DeviceRequest, PciId}; /// One entry of a row's `devices` or `drives`. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Entry { - /// A class, a partition or an ISA set: one device by its own name. + /// A class or an ISA set: one device by its own name. Fixed(DeviceRequest), Pci(PciMatch), } @@ -35,7 +36,7 @@ impl Entry { match (self, other) { (Self::Fixed(a), Self::Fixed(b)) => a == b, (Self::Pci(a), Self::Pci(b)) => { - a.class == b.class && either(a.prog_if, b.prog_if) && either(a.id, b.id) + a.class == b.class && either(a.prog_if, b.prog_if) && either(a.id, b.id) && either(a.at, b.at) } _ => false, } @@ -59,6 +60,8 @@ pub struct PciMatch { /// Audio's 00 and 80) is driven alike whichever a function reports. pub prog_if: Option, pub id: Option, + /// Bus, device and function, on segment 0. + pub at: Option<(u8, u8, u8)>, } impl PciMatch { @@ -66,17 +69,29 @@ impl PciMatch { /// Every function of class code `class`, of card `id` where it names one. pub const fn new(class: [u8; 3], id: Option) -> Self { - Self { class: [class[0], class[1]], prog_if: Some(class[2]), id } + Self { class: [class[0], class[1]], prog_if: Some(class[2]), id, at: None } } /// Every function of `class`'s base class and subclass, whatever its /// programming interface. pub const fn any_interface(class: [u8; 2], id: Option) -> Self { - Self { class, prog_if: None, id } + Self { class, prog_if: None, id, at: None } } pub fn parse(text: &str) -> Option { let rest = text.strip_prefix(Self::PREFIX)?; + let (rest, at) = match rest.split_once('@') { + Some((rest, place)) => { + let (bus, slot) = place.split_once(':')?; + let (dev, func) = slot.split_once('.')?; + let func = match func.as_bytes() { + [digit @ b'0'..=b'7'] => digit - b'0', + _ => return None, + }; + (rest, Some((hex8(bus)?, hex8(dev).filter(|d| *d < 32)?, func))) + } + None => (rest, None), + }; let (code, card) = match rest.split_once('/') { Some((code, card)) => { let (vendor, device) = card.split_once(':')?; @@ -93,13 +108,14 @@ impl PciMatch { if fields.next().is_some() { return None; } - Some(Self { class, prog_if, id: card }) + Some(Self { class, prog_if, id: card, at }) } pub fn matches(&self, pci: &Pci) -> bool { [pci.class, pci.subclass] == self.class && self.prog_if.is_none_or(|prog_if| prog_if == pci.prog_if) && self.id.is_none_or(|id| id.vendor == pci.vendor && id.device == pci.device) + && self.at.is_none_or(|at| pci.at.segment == 0 && (pci.at.bus, pci.at.dev, pci.at.func) == at) } } @@ -111,8 +127,11 @@ impl fmt::Display for PciMatch { Some(prog_if) => write!(f, "{prog_if:02x}")?, None => f.write_str("*")?, } - match self.id { - Some(id) => write!(f, "/{:04x}:{:04x}", id.vendor, id.device), + if let Some(id) = self.id { + write!(f, "/{:04x}:{:04x}", id.vendor, id.device)?; + } + match self.at { + Some((bus, dev, func)) => write!(f, "@{bus:02x}:{dev:02x}.{func}"), None => Ok(()), } } @@ -161,7 +180,8 @@ mod tests { #[test] fn a_matcher_writes_back_the_text_it_parsed() { for text in - ["pci:01:08:02/1b36:0010", "pci:0c:03:30", "pci:ff:00:00/1234:11e8", "pci:00:00:00", "pci:04:03:*", "pci:04:03:*/8086:a0c8"] + ["pci:01:08:02/1b36:0010", "pci:0c:03:30", "pci:ff:00:00/1234:11e8", "pci:00:00:00", "pci:04:03:*", "pci:04:03:*/8086:a0c8", + "pci:0c:03:30/1022:1639@03:00.3", "pci:0c:03:30@00:14.0"] { let parsed = PciMatch::parse(text).unwrap_or_else(|| panic!("{text} did not parse")); assert_eq!(parsed.to_string(), text); @@ -183,6 +203,9 @@ mod tests { // spellings would be two labels for one claim. "pci:02:00:00/1Af4:1041", "pci:02:00:00/1af4:41", "pci:02:00:00/1af4", "pci:02:00:00/1af4:1041:", "pci:02:00:00/:", "pci:02:00:00/zzzz:1041", "pci:02:00:00/1af4:104g", "pci:02:00:00/+af4:1041", + // A place is two digits, two digits below 0x20, and one octal one. + "pci:0c:03:30@3:00.3", "pci:0c:03:30@03:20.0", "pci:0c:03:30@03:00.8", "pci:0c:03:30@03:00", "pci:0c:03:30@03:00.3/1022:1639", + "pci:0c:03:30@", "pci:0c:03:30@03:0A.0", ] { assert_eq!(PciMatch::parse(bad), None, "{bad} parsed"); assert_eq!(Entry::parse(bad), None, "{bad} parsed as an entry"); @@ -191,7 +214,7 @@ mod tests { #[test] fn a_fixed_entry_is_the_abis_spelling() { - assert_eq!(Entry::parse("keyboard"), Some(Entry::Fixed(DeviceRequest::Class(DeviceType::Keyboard)))); + assert_eq!(Entry::parse("framebuffer"), Some(Entry::Fixed(DeviceRequest::Class(DeviceType::Framebuffer)))); assert!(matches!(Entry::parse("isa:0060,0064:1,12"), Some(Entry::Fixed(DeviceRequest::Isa(_))))); assert_eq!(Entry::parse("gpu"), None); } @@ -205,6 +228,20 @@ mod tests { assert!(any_nvme.matches(&function([0x01, 0x08, 0x02], 0x144d, 0xa808))); } + /// A place tells two functions of one card apart, and two matchers of + /// one card at two places name no function in common. + #[test] + fn a_place_matches_its_function_alone() { + let at = |func: u8| Pci { at: PciAddr { segment: 0, bus: 3, dev: 0, func }, ..function([0x0c, 0x03, 0x30], 0x1022, 0x1639) }; + let three = PciMatch::parse("pci:0c:03:30/1022:1639@03:00.3").unwrap(); + let four = PciMatch::parse("pci:0c:03:30/1022:1639@03:00.4").unwrap(); + assert!(three.matches(&at(3)) && !three.matches(&at(4))); + assert!(!Entry::Pci(three).overlaps(&Entry::Pci(four))); + assert!(Entry::Pci(three).overlaps(&Entry::Pci(PciMatch::parse("pci:0c:03:30").unwrap()))); + let elsewhere = Pci { at: PciAddr { segment: 1, bus: 3, dev: 0, func: 3 }, ..at(3) }; + assert!(!three.matches(&elsewhere), "a place is on segment 0"); + } + /// The T14's HD Audio controller reports programming interface 0x80 and /// QEMU's 0x00: one matcher names both, and nothing outside the subclass. #[test] @@ -230,7 +267,6 @@ mod tests { ("pci:04:03:00/8086:293e", "pci:04:03:*"), ("framebuffer", "framebuffer"), ("isa:0060,0064:1,12", "isa:0060,0064:1,12"), - ("part:0FC63DAF-8483-4772-8E79-3D69D8477DE4", "part:0FC63DAF-8483-4772-8E79-3D69D8477DE4"), ]; for (a, b) in overlapping { assert!(entry(a).overlaps(&entry(b)), "{a} and {b}"); @@ -240,8 +276,8 @@ mod tests { ("pci:01:08:02", "pci:01:08:01"), ("pci:04:03:*", "pci:04:01:00"), ("pci:01:08:02/1b36:0010", "pci:01:08:01/1b36:0010"), - ("framebuffer", "keyboard"), - ("pci:0c:03:30", "keyboard"), + ("framebuffer", "acpi"), + ("pci:0c:03:30", "acpi"), ]; for (a, b) in apart { assert!(!entry(a).overlaps(&entry(b)), "{a} and {b}"); diff --git a/lib/manifest/src/lib.rs b/lib/manifest/src/lib.rs index 3055f855a3e..a230b27c58a 100644 --- a/lib/manifest/src/lib.rs +++ b/lib/manifest/src/lib.rs @@ -624,7 +624,7 @@ mod tests { path: "/system/bin/compositor".into(), serves: vec!["compositor".into()], receives: vec!["soundserver".into()], - devices: vec!["framebuffer".into(), "keyboard".into()], + devices: vec!["framebuffer".into(), "acpi".into()], service: true, starts: vec!["terminal".into(), "/apps".into()], login: true, @@ -951,14 +951,9 @@ mod tests { /// and a `drives` matcher alike, and a fixed name written twice. #[test] fn two_entries_one_device_can_answer_are_refused_by_name() { - let cases: [(&[Row<'_>], _, _); 7] = [ + let cases: [(&[Row<'_>], _, _); 6] = [ (&[("a", &["framebuffer"], &[]), ("b", &["framebuffer"], &[])], ("a", "framebuffer"), ("b", "framebuffer")), (&[("a", &["isa:0060,0064:1,12"], &[]), ("b", &["isa:0060,0064:1,12"], &[])], ("a", "isa:0060,0064:1,12"), ("b", "isa:0060,0064:1,12")), - ( - &[("a", &["part:0FC63DAF-8483-4772-8E79-3D69D8477DE4"], &[]), ("b", &["part:0FC63DAF-8483-4772-8E79-3D69D8477DE4"], &[])], - ("a", "part:0FC63DAF-8483-4772-8E79-3D69D8477DE4"), - ("b", "part:0FC63DAF-8483-4772-8E79-3D69D8477DE4"), - ), (&[("a", &["pci:01:08:02/1b36:0010"], &[]), ("b", &[], &["pci:01:08:02"])], ("a", "pci:01:08:02/1b36:0010"), ("b", "pci:01:08:02")), (&[("a", &[], &["pci:0c:03:30"]), ("b", &[], &["pci:0c:03:30/1b36:000d"])], ("a", "pci:0c:03:30"), ("b", "pci:0c:03:30/1b36:000d")), (&[("a", &["pci:02:00:00", "pci:02:00:00/1af4:1041"], &[])], ("a", "pci:02:00:00"), ("a", "pci:02:00:00/1af4:1041")), @@ -969,7 +964,7 @@ mod tests { } let apart = with_rows(&[ ("a", &["framebuffer", "pci:01:08:02/1b36:0010"], &[]), - ("b", &["keyboard", "pci:01:08:02/144d:a808"], &[]), + ("b", &["acpi", "pci:01:08:02/144d:a808"], &[]), ("c", &[], &["pci:0c:03:30"]), ]); assert!(render(&apart).is_ok()); @@ -980,7 +975,7 @@ mod tests { for (devices, drives, entry) in [ (&["gpu"][..], &[][..], "gpu"), (&["pci:1af4:1041"], &[], "pci:1af4:1041"), - (&[], &["keyboard"], "keyboard"), + (&[], &["acpi"], "acpi"), (&[], &["pci:01:08"], "pci:01:08"), ] { let refused = RenderError::NoSuchDevice { program: "a".into(), entry: entry.into() }; @@ -1001,7 +996,7 @@ mod tests { assert!(drives(|_| {}).is_ok()); assert_eq!(drives(|m| m.programs[0].serves = vec!["edu".into()]), refused("serves")); assert_eq!(drives(|m| m.programs[0].roles = vec!["data".into()]), refused("roles")); - assert_eq!(drives(|m| m.programs[0].devices = vec!["keyboard".into()]), refused("devices")); + assert_eq!(drives(|m| m.programs[0].devices = vec!["acpi".into()]), refused("devices")); assert_eq!(drives(|m| m.start = vec!["a".into()]), refused("[boot] start")); assert_eq!(drives(|m| m.programs[0].starts = vec!["a".into()]), refused("starts")); } diff --git a/lib/quiesce/src/lib.rs b/lib/quiesce/src/lib.rs index 25e09c1bbe0..20ac4949bb2 100644 --- a/lib/quiesce/src/lib.rs +++ b/lib/quiesce/src/lib.rs @@ -79,6 +79,16 @@ pub const FLUSH_MS: u64 = 5_000; /// together after the flush, before a stop. pub const SYNC_MS: u64 = 5_000; +/// A policy number: how long the supervisor waits for every disk driver to end once +/// told the machine stops, after the sync. Above usbd's own bound on its stop +/// (`toyos_xhci::hc::STOP_NS`), so a driver that keeps its bound is waited out. +pub const DRIVERS_MS: u64 = 3_000; + +/// The endowment a disk driver is told the machine stops by: a pipe the supervisor +/// closes once every file server has synced. The driver hands its devices back, +/// leaves its controller as firmware would find it, and ends. +pub const DRIVER_STOP: &str = "stop"; + /// The thread the kernel's quiesce-last actuators hold, by the name its /// program gives it: held until it is the one thread the stop still waits /// on, so the transition it makes next is the stop's last. @@ -92,8 +102,7 @@ const THREADS: &str = " userland thread(s) stopped across "; const CPUS: &str = " cpu(s) in "; const BUDGET: &str = " ms of a "; const MS: &str = " ms budget over "; -const SWEEPS: &str = " sweep(s), "; -const OPEN: &str = " userland block operation(s) still open"; +const SWEEPS: &str = " sweep(s)"; const SHORTFALL: &str = "; this reset lands wherever the other "; const ARE: &str = " are"; @@ -109,14 +118,6 @@ pub struct Record { pub budget_ms: u64, pub sweeps: u32, pub cpus: u32, - /// Block-device operations still open on a thread the stop stops: an - /// operation lasts only while its opener is inside the device, so a thread - /// that has stopped holds none. - pub in_flight: u32, - /// How many such operations this boot began. Zero says the counter behind - /// [`Self::in_flight`] never counted at all, which no boot that wrote a - /// file can honestly report. - pub begun: u64, } impl Record { @@ -134,8 +135,6 @@ impl Record { let (elapsed_ms, rest) = rest.split_once(BUDGET)?; let (budget_ms, rest) = rest.split_once(MS)?; let (sweeps, rest) = rest.split_once(SWEEPS)?; - let (in_flight, rest) = rest.split_once(OF)?; - let (begun, rest) = rest.split_once(OPEN)?; let stopped: u32 = stopped.parse().ok()?; let running = total.parse::().ok()?.checked_sub(stopped)?; @@ -153,8 +152,6 @@ impl Record { budget_ms: budget_ms.parse().ok()?, sweeps: sweeps.parse().ok()?, cpus: cpus.parse().ok()?, - in_flight: in_flight.parse().ok()?, - begun: begun.parse().ok()?, }) } @@ -168,15 +165,13 @@ impl fmt::Display for Record { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "{STOPPED}{}{OF}{}{THREADS}{}{CPUS}{}{BUDGET}{}{MS}{}{SWEEPS}{}{OF}{}{OPEN}", + "{STOPPED}{}{OF}{}{THREADS}{}{CPUS}{}{BUDGET}{}{MS}{}{SWEEPS}", self.sweep.stopped, self.sweep.total(), self.cpus, self.elapsed_ms, self.budget_ms, self.sweeps, - self.in_flight, - self.begun, )?; if self.sweep.running > 0 { write!(f, "{SHORTFALL}{}{ARE}", self.sweep.running)?; @@ -242,12 +237,10 @@ mod tests { budget_ms: 2010, sweeps: 3, cpus: 8, - in_flight: 0, - begun: 4812, }; fn short() -> Record { - Record { sweep: Sweep { stopped: 4, running: 2 }, in_flight: 1, ..WHOLE } + Record { sweep: Sweep { stopped: 4, running: 2 }, ..WHOLE } } #[test] @@ -255,12 +248,12 @@ mod tests { assert_eq!( alloc::format!("{WHOLE}"), "stop: 6 of 6 userland thread(s) stopped across 8 cpu(s) in 11 ms of a 2010 ms \ - budget over 3 sweep(s), 0 of 4812 userland block operation(s) still open", + budget over 3 sweep(s)", ); assert_eq!( alloc::format!("{}", short()), "stop: 4 of 6 userland thread(s) stopped across 8 cpu(s) in 11 ms of a 2010 ms \ - budget over 3 sweep(s), 1 of 4812 userland block operation(s) still open; this \ + budget over 3 sweep(s); this \ reset lands wherever the other 2 are", ); assert!(WHOLE.stopped_the_machine()); @@ -285,15 +278,15 @@ mod tests { "[stamp] stop: 6 of 6 userland thread(s) stopped across 8 cpu(s)", // The shortfall clause disagreeing with the counts it restates. "[stamp] stop: 4 of 6 userland thread(s) stopped across 8 cpu(s) in 11 ms of a 2010 \ - ms budget over 3 sweep(s), 1 of 4812 userland block operation(s) still open; this \ + ms budget over 3 sweep(s); this \ reset lands wherever the other 9 are", // A shortfall clause on a record that claims to have stopped. "[stamp] stop: 6 of 6 userland thread(s) stopped across 8 cpu(s) in 11 ms of a 2010 \ - ms budget over 3 sweep(s), 0 of 4812 userland block operation(s) still open; this \ + ms budget over 3 sweep(s); this \ reset lands wherever the other 2 are", // More threads stopped than there were. "[stamp] stop: 7 of 6 userland thread(s) stopped across 8 cpu(s) in 11 ms of a 2010 \ - ms budget over 3 sweep(s), 0 of 4812 userland block operation(s) still open", + ms budget over 3 sweep(s)", ] { assert_eq!(Record::parse(line), None, "{line}"); } diff --git a/lib/trace/src/lib.rs b/lib/trace/src/lib.rs index 34a2014ea52..15e3abb1afb 100644 --- a/lib/trace/src/lib.rs +++ b/lib/trace/src/lib.rs @@ -41,7 +41,6 @@ pub enum Event { TimerStop, TimerFire, TimerFireBurst { fires: u32 }, - IrqDrain { source: u8, latency_us: u32 }, Mark { index: u32 }, } @@ -66,7 +65,6 @@ impl Event { Self::TimerStop => "timer-stop", Self::TimerFire => "timer-fire", Self::TimerFireBurst { .. } => "timer-fires", - Self::IrqDrain { .. } => "irq-drain", Self::Mark { .. } => "mark", } } @@ -120,7 +118,6 @@ impl Entry { Kind::TimerStop => Event::TimerStop, Kind::TimerFire => Event::TimerFire, Kind::TimerFireBurst => Event::TimerFireBurst { fires: raw.data }, - Kind::IrqDrain => Event::IrqDrain { source: (raw.data >> 24) as u8, latency_us: raw.data & 0x00FF_FFFF }, Kind::Mark => Event::Mark { index: raw.data }, }; let thread = (raw.pid != NO_THREAD || raw.tid != NO_THREAD).then_some(Thread { pid: raw.pid, tid: raw.tid }); @@ -156,7 +153,6 @@ impl fmt::Display for Line<'_> { Event::Migrate { to } => write!(f, " to=cpu{to}"), Event::TimerArm { span_ns } => write!(f, " in={span_ns}ns"), Event::TimerFireBurst { fires } => write!(f, " fires={fires}"), - Event::IrqDrain { source, latency_us } => write!(f, " source={source} after={latency_us}us"), Event::Mark { index } => write!(f, " index={index}"), Event::Pick | Event::Wake @@ -233,7 +229,6 @@ mod tests { (Kind::TimerArm, 250_000, Event::TimerArm { span_ns: 250_000 }, " in=250000ns"), (Kind::TimerStop, 0, Event::TimerStop, ""), (Kind::TimerFireBurst, 3, Event::TimerFireBurst { fires: 3 }, " fires=3"), - (Kind::IrqDrain, 2 << 24 | 13, Event::IrqDrain { source: 2, latency_us: 13 }, " source=2 after=13us"), (Kind::Mark, 41, Event::Mark { index: 41 }, " index=41"), ]; for (kind, data, event, tail) in cases { diff --git a/lib/usbhid/Cargo.toml b/lib/usbhid/Cargo.toml new file mode 100644 index 00000000000..7a26db4d4ca --- /dev/null +++ b/lib/usbhid/Cargo.toml @@ -0,0 +1,15 @@ +# A member of the host workspace (root `Cargo.toml`): usbd depends on it by +# path and its tests run on the host. + +[package] +name = "toyos-usbhid" +description = "USB HID report descriptors read into keyboards and pointers, and their reports into key transitions and pointer reports, every malformed descriptor and report refused by name; pure." +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" + +# One keyboard's held set, and the `input` record a pointer report is bounded +# to. +[dependencies] +toyos-input = { path = "../input" } +toyos-abi = { path = "../../sdk/abi" } diff --git a/lib/usbhid/src/descriptor.rs b/lib/usbhid/src/descriptor.rs new file mode 100644 index 00000000000..0a59ec40388 --- /dev/null +++ b/lib/usbhid/src/descriptor.rs @@ -0,0 +1,448 @@ +//! A HID report descriptor (HID 1.11 §6.2.2), read into the input fields of +//! each top-level application collection. +//! +//! **Every byte is the device's**, so every length and value is bounded before +//! it is used, and nothing here panics or loops on a device's word. What +//! breaks the reading itself — an item running past the end, a collection +//! closed that was never opened or never closed, a Push past the stack, Report +//! ID 0, a main item outside every collection — refuses the interface by name +//! ([`Refused`]). +//! +//! **A report belongs to one top-level collection** (§8.4: "reports may not +//! span more than one top level collection"), so what this reader cannot +//! bind costs only its own collection, whose reports are the Report IDs its +//! fields carry (§6.2.2.7): [`Unbound`]. A Report ID two top-level collections +//! both carry is a descriptor §8.4 forbids, and binding one half would read +//! the other's fields as absent: the interface is refused. + +use alloc::vec::Vec; + +/// The longest input report, its Report ID byte included, this reader binds: +/// past it the collection is not bound. A full-speed interrupt endpoint moves +/// at most 64 bytes a packet (USB 2.0 §5.7.3), so a longer report spans +/// transfers. +pub const MAX_REPORT_BYTES: usize = 64; + +/// HID Usage Tables 1.5's pages and the usages this crate binds: Generic +/// Desktop (§4), Keyboard/Keypad (§10), Button (§12). +pub const GENERIC_DESKTOP: u16 = 0x01; +pub const KEYS: u16 = 0x07; +pub const BUTTON: u16 = 0x09; +pub const POINTER: u16 = 0x01; +pub const MOUSE: u16 = 0x02; +pub const KEYBOARD: u16 = 0x06; +pub const KEYPAD: u16 = 0x07; +pub const X: u16 = 0x30; +pub const Y: u16 = 0x31; +pub const WHEEL: u16 = 0x38; + +/// Push's depth; no descriptor in reach pushes more than twice. +const GLOBAL_DEPTH: usize = 4; +/// Collections nested inside one another. +const NESTING: usize = 8; +/// Usages one main item's locals may list one by one. +const LISTED: usize = 64; +/// Top-level collections one interface may declare. +const COLLECTIONS: usize = 16; +/// How far an array's usage list is walked to say what it names: past the +/// whole Keyboard/Keypad page. +const LIST_WALK: i32 = 0x100; + +/// Why an interface's descriptor is not read at all. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Refused { + /// An item whose data runs past the descriptor's end. + Truncated { at: usize }, + /// An End Collection with no collection open, or a collection never closed. + Unbalanced { at: usize }, + /// Collections nested deeper than this reader follows, or more top-level + /// ones than it keeps. + TooDeep { at: usize }, + /// A Push past the global stack, or a Pop of an empty one. + GlobalStack { at: usize }, + /// A main item outside every collection. + Outside { at: usize }, + /// Report ID 0, which §6.2.2.7 reserves, or one past a byte. + ReportId { at: usize }, + /// A field with no Report ID in a descriptor that numbers its reports: + /// every report then begins with its ID (§6.2.2.7). + Unnumbered, + /// A Report ID two top-level collections both carry (§8.4). + SpansCollections { id: u8 }, +} + +/// Why one collection is not bound: its reports are counted and dropped. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Unbound { + /// Neither a keyboard's keys nor a pointer's axes: a consumer control, a + /// vendor's collection, a UPS. + NotAKeyboardOrPointer { page: u16, usage: u16 }, + /// An input report longer than [`MAX_REPORT_BYTES`]. + ReportTooLong { id: u8 }, + /// A Report Size of 0 or past 32 bits. + ReportSize { bits: u32 }, + /// More usages listed one by one than this reader keeps. + TooManyUsages, + /// An axis or a key field whose Logical Maximum is not above its Logical + /// Minimum: nothing maps its values. + EmptyRange { page: u16, usage: u16 }, +} + +/// The usages one field's controls name, in control order (§6.2.2.8). +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum Usages { + /// Usage Minimum to Usage Maximum. + Range { first: u32, last: u32 }, + /// Usage items one by one. + Listed(Vec), +} + +impl Usages { + /// Entry `index` of the list, or `None` past its end. An extended usage + /// carries its page in its top half. + pub fn at(&self, index: u32) -> Option { + match self { + Self::Range { first, last } => first.checked_add(index).filter(|u| u <= last), + Self::Listed(list) => list.get(index as usize).copied(), + } + } +} + +/// One Input main item's controls. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Field { + /// Its report's ID, 0 where the interface numbers none. + pub report: u8, + /// Where its first control starts, in bits past the report's ID byte. + pub bit: u32, + pub size: u32, + pub count: u32, + /// Data, not constant padding. + pub data: bool, + /// A variable field's controls are one usage each; an array field's are + /// indexes into its usages (§6.2.2.5). + pub variable: bool, + pub relative: bool, + pub page: u16, + pub usages: Usages, + pub logical_min: i32, + pub logical_max: i32, +} + +impl Field { + /// The page and usage control `index` of a variable field names. Past a + /// list given one by one, the last usage repeats for every control + /// (§6.2.2.8). + pub fn control(&self, index: u32) -> Option<(u16, u16)> { + let usage = match &self.usages { + Usages::Range { .. } => self.usages.at(index), + Usages::Listed(list) => list.get(index as usize).or(list.last()).copied(), + }; + usage.map(|u| self.page_of(u)) + } + + /// What an array field's control value `value` selects: `None` for a value + /// outside Logical Minimum to Maximum, which asserts nothing (§6.2.2.5), + /// `Some(None)` for an index past the field's usage list, and otherwise + /// the usage at index `value - Logical Minimum`. + pub fn selected(&self, value: i32) -> Option> { + if !self.holds(value) { + return None; + } + let index = (i64::from(value) - i64::from(self.logical_min)) as u32; + Some(self.usages.at(index).map(|u| self.page_of(u))) + } + + /// Whether `value` is inside Logical Minimum to Maximum. + pub fn holds(&self, value: i32) -> bool { + (self.logical_min..=self.logical_max).contains(&value) + } + + /// Control `index`'s value in `report`, the bytes past its ID: signed + /// where Logical Minimum is negative (§6.2.2.7), or `None` for a report + /// too short to hold it. + pub fn value(&self, report: &[u8], index: u32) -> Option { + let first = u64::from(self.bit) + u64::from(index) * u64::from(self.size); + let mut raw = 0u64; + for n in 0..u64::from(self.size) { + let at = first + n; + let byte = *report.get(usize::try_from(at / 8).ok()?)?; + raw |= u64::from((byte >> (at % 8)) & 1) << n; + } + if self.logical_min < 0 && self.size > 0 && raw >> (self.size - 1) & 1 == 1 { + raw |= u64::MAX << self.size; + } + Some(raw as i32) + } + + /// The bits the field's controls end at, past the report's ID byte. + pub fn end(&self) -> u64 { + u64::from(self.bit) + u64::from(self.size) * u64::from(self.count) + } + + fn page_of(&self, usage: u32) -> (u16, u16) { + match usage >> 16 { + 0 => (self.page, usage as u16), + page => (page as u16, usage as u16), + } + } + + /// Whether any control of this field names `page`'s `usage`. + fn names(&self, page: u16, usage: u16) -> bool { + if self.variable { + (0..self.count).any(|i| self.control(i) == Some((page, usage))) + } else { + (0..=self.logical_max.saturating_sub(self.logical_min).clamp(0, LIST_WALK)) + .any(|i| self.usages.at(i as u32).map(|u| self.page_of(u)) == Some((page, usage))) + } + } + + /// Whether this field reports keys: a Keyboard/Keypad page usage. + pub fn keys(&self) -> bool { + self.page == KEYS || self.usages.at(0).is_some_and(|u| u >> 16 == u32::from(KEYS)) + } +} + +/// What a bound collection is to the owner of input. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Kind { + Keyboard, + Pointer, +} + +/// One top-level application collection, and every input field inside it. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Collection { + pub page: u16, + pub usage: u16, + /// Report IDs its fields carry; empty where the interface numbers none. + pub reports: Vec, + pub fields: Vec, + pub bound: Result, +} + +/// A whole interface's descriptor. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Interface { + pub collections: Vec, + /// Whether every report begins with its Report ID. + pub numbered: bool, +} + +impl Interface { + /// The collection report `id` belongs to; for an interface that numbers + /// none, its one collection with input fields. + pub fn of(&self, id: u8) -> Option { + self.collections + .iter() + .position(|c| if self.numbered { c.reports.contains(&id) } else { !c.fields.is_empty() }) + } +} + +#[derive(Clone, Copy, Default)] +struct Globals { + page: u16, + logical_min: i32, + logical_max: i32, + size: u32, + count: u32, + report: u8, +} + +#[derive(Default)] +struct Locals { + listed: Vec, + min: Option, + max: Option, + too_many: bool, +} + +/// A 4-byte usage is extended and keeps its page; a shorter one is on the +/// Usage Page in force (§6.2.2.8). +fn usage(len: usize, unsigned: u32) -> u32 { + if len == 4 { unsigned } else { unsigned & 0xFFFF } +} + +/// Read `bytes`, the descriptor a HID interface answered +/// GET_DESCRIPTOR(Report) with. +pub fn parse(bytes: &[u8]) -> Result { + let mut globals = Globals::default(); + let mut stack: Vec = Vec::new(); + let mut locals = Locals::default(); + let mut depth = 0usize; + let mut collections: Vec = Vec::new(); + // What stops each collection binding, found while it was read. + let mut flaws: Vec> = Vec::new(); + // Input bits so far of each report, by ID. + let mut lengths: Vec<(u8, u32)> = Vec::new(); + let mut numbered = false; + let mut at = 0usize; + while at < bytes.len() { + let prefix = bytes[at]; + // A long item (§6.2.2.3): no tag is defined, so it is stepped over. + if prefix == 0xFE { + let len = usize::from(*bytes.get(at + 1).ok_or(Refused::Truncated { at })?); + at = Some(at + 3 + len).filter(|end| *end <= bytes.len()).ok_or(Refused::Truncated { at })?; + continue; + } + let len = [0, 1, 2, 4][usize::from(prefix & 0x3)]; + let data = bytes.get(at + 1..at + 1 + len).ok_or(Refused::Truncated { at })?; + let unsigned = data.iter().rev().fold(0u32, |v, b| v << 8 | u32::from(*b)); + let signed = match len { + 1 => i32::from(data[0] as i8), + 2 => i32::from(i16::from_le_bytes([data[0], data[1]])), + 4 => unsigned as i32, + _ => 0, + }; + match (prefix >> 2 & 0x3, prefix >> 4) { + // Input (§6.2.2.4). + (0, 0x8) => { + if depth == 0 { + return Err(Refused::Outside { at }); + } + let open = collections.len() - 1; + if globals.size == 0 || globals.size > 32 { + flaws[open].get_or_insert(Unbound::ReportSize { bits: globals.size }); + } + if locals.too_many { + flaws[open].get_or_insert(Unbound::TooManyUsages); + } + let usages = match (locals.min, locals.max) { + (Some(first), Some(last)) if locals.listed.is_empty() => Usages::Range { first, last }, + _ => Usages::Listed(core::mem::take(&mut locals.listed)), + }; + let index = match lengths.iter().position(|(id, _)| *id == globals.report) { + Some(index) => index, + None => { + lengths.push((globals.report, 0)); + lengths.len() - 1 + } + }; + let bits = &mut lengths[index].1; + let field = Field { + report: globals.report, + bit: *bits, + size: globals.size, + count: globals.count, + data: unsigned & 0x1 == 0, + variable: unsigned & 0x2 != 0, + relative: unsigned & 0x4 != 0, + page: globals.page, + usages, + logical_min: globals.logical_min, + logical_max: globals.logical_max, + }; + *bits = bits.saturating_add(globals.size.saturating_mul(globals.count)); + let collection = &mut collections[open]; + if globals.report != 0 && !collection.reports.contains(&globals.report) { + collection.reports.push(globals.report); + } + collection.fields.push(field); + locals = Locals::default(); + } + // Output and Feature carry no input bits: only their locals end. + (0, 0x9) | (0, 0xB) => locals = Locals::default(), + (0, 0xA) => { + if depth == NESTING || (depth == 0 && collections.len() == COLLECTIONS) { + return Err(Refused::TooDeep { at }); + } + // A collection at the top opens a device; any inside it is + // part of that device. + if depth == 0 { + let (page, usage) = match locals.listed.first().or(locals.min.as_ref()) { + Some(&u) if u >> 16 != 0 => ((u >> 16) as u16, u as u16), + Some(&u) => (globals.page, u as u16), + None => (globals.page, 0), + }; + let bound = Err(Unbound::NotAKeyboardOrPointer { page, usage }); + collections.push(Collection { page, usage, reports: Vec::new(), fields: Vec::new(), bound }); + flaws.push(None); + } + depth += 1; + locals = Locals::default(); + } + (0, 0xC) => { + depth = depth.checked_sub(1).ok_or(Refused::Unbalanced { at })?; + locals = Locals::default(); + } + // Global items (§6.2.2.7). + (1, 0x0) => globals.page = unsigned as u16, + (1, 0x1) => globals.logical_min = signed, + // A maximum that reads below a non-negative minimum as signed is + // unsigned: its top bit is magnitude (§6.2.2.7's remarks). + (1, 0x2) => { + globals.logical_max = + if signed < globals.logical_min && globals.logical_min >= 0 { unsigned as i32 } else { signed } + } + (1, 0x7) => globals.size = unsigned, + (1, 0x8) => { + if unsigned == 0 || unsigned > 0xFF { + return Err(Refused::ReportId { at }); + } + globals.report = unsigned as u8; + numbered = true; + } + (1, 0x9) => globals.count = unsigned, + (1, 0xA) => { + if stack.len() == GLOBAL_DEPTH { + return Err(Refused::GlobalStack { at }); + } + stack.push(globals); + } + (1, 0xB) => globals = stack.pop().ok_or(Refused::GlobalStack { at })?, + // Local items (§6.2.2.8). + (2, 0x0) => match locals.listed.len() { + LISTED => locals.too_many = true, + _ => locals.listed.push(usage(len, unsigned)), + }, + (2, 0x1) => locals.min = Some(usage(len, unsigned)), + (2, 0x2) => locals.max = Some(usage(len, unsigned)), + // Unit, exponent, physical extent, designators, strings, + // delimiters and the reserved tags say nothing this reader binds. + _ => {} + } + at += 1 + len; + } + if depth != 0 { + return Err(Refused::Unbalanced { at }); + } + if numbered && collections.iter().any(|c| c.fields.iter().any(|f| f.report == 0)) { + return Err(Refused::Unnumbered); + } + for (i, collection) in collections.iter().enumerate() { + let shared = collection.reports.iter().find(|id| collections[i + 1..].iter().any(|c| c.reports.contains(id))); + if let Some(&id) = shared { + return Err(Refused::SpansCollections { id }); + } + } + for (collection, flaw) in collections.iter_mut().zip(flaws) { + collection.bound = match flaw { + Some(flaw) => Err(flaw), + None => bind(collection, &lengths, numbered), + }; + } + Ok(Interface { collections, numbered }) +} + +/// What `collection` is, from its application usage and the fields it +/// carries, or why it is not bound. +fn bind(collection: &Collection, lengths: &[(u8, u32)], numbered: bool) -> Result { + let its = |id: &u8| !numbered || collection.reports.contains(id); + if let Some(&(id, _)) = lengths + .iter() + .find(|(id, bits)| its(id) && (*bits as usize).div_ceil(8) + usize::from(numbered) > MAX_REPORT_BYTES) + { + return Err(Unbound::ReportTooLong { id }); + } + let data = || collection.fields.iter().filter(|f| f.data); + let axis = |f: &Field| f.names(GENERIC_DESKTOP, X) || f.names(GENERIC_DESKTOP, Y); + if let Some(f) = data().find(|f| f.logical_max <= f.logical_min && (f.keys() || axis(f))) { + let (page, usage) = f.control(0).unwrap_or((f.page, 0)); + return Err(Unbound::EmptyRange { page, usage }); + } + match (collection.page, collection.usage) { + (GENERIC_DESKTOP, KEYBOARD | KEYPAD) if data().any(Field::keys) => Ok(Kind::Keyboard), + (GENERIC_DESKTOP, MOUSE | POINTER) if data().any(axis) => Ok(Kind::Pointer), + (page, usage) => Err(Unbound::NotAKeyboardOrPointer { page, usage }), + } +} diff --git a/lib/usbhid/src/lib.rs b/lib/usbhid/src/lib.rs new file mode 100644 index 00000000000..ad269d081dd --- /dev/null +++ b/lib/usbhid/src/lib.rs @@ -0,0 +1,19 @@ +//! USB HID reports in, key transitions and pointer reports out, through each +//! interface's own report descriptor. +//! +//! Every byte here is one a device chose. [`descriptor::parse`] reads the +//! descriptor into its top-level collections, refusing by name what it cannot +//! read, and [`report::Hid`] reads each report through it: one that does not +//! read as its descriptor says is refused by name and changes no state, and +//! no input panics. Nothing here touches a device or holds a lock. + +#![no_std] +#![forbid(unsafe_code)] + +extern crate alloc; + +pub mod descriptor; +pub mod report; + +#[cfg(test)] +mod tests; diff --git a/lib/usbhid/src/report.rs b/lib/usbhid/src/report.rs new file mode 100644 index 00000000000..08cff849faa --- /dev/null +++ b/lib/usbhid/src/report.rs @@ -0,0 +1,313 @@ +//! One interface's input reports, read through its report descriptor into +//! key transitions and pointer reports for each bound collection. +//! +//! **A key field is read as HID 1.11 and the HID Usage Tables 1.5 define it.** +//! - An array field reports indexes (§6.2.2.5): a value outside Logical +//! Minimum to Maximum asserts nothing, and one inside names the usage at +//! index `value - Logical Minimum` of the field's list; an index past the +//! list names nothing and is counted [`Counts::unlisted`]. +//! - A variable field is a bitmap: a control is held at 1 and released at 0 +//! (§5.9), and a value outside its logical range leaves it as it was (§5.10). +//! - Usages `00` to `03` are never keys: `00` is an empty slot, and `01` to +//! `03` are "sent as a member of the keyboard array. Not a physical key" +//! (HUT 1.5 p.96, note 1). In an array, ErrorRollOver (`01`) is Appendix C's +//! phantom state, in which the report's variable fields are believed and its +//! arrays are not, so the keys an array held stay held; POSTFail (`02`) and +//! ErrorUndefined (`03`) refuse the report. +//! - A usage HUT 1.5 reserves — `A5-AF` (p.94), `DE-DF` (p.95) and `E8-FFFF` +//! (p.96) — names no key and is counted [`Counts::reserved`]. +//! +//! **A report says only what its own fields cover.** A keyboard whose keys +//! come in two reports releases, on each, only the usages that report's +//! fields can name. +//! +//! **Every pointer field is bounded to the `input` record** (`toyos-input`'s +//! wire): the Button page's usages 1 to 5 are its five buttons, and one past +//! them pressed is counted [`Counts::unsent_button`]; relative motion and the +//! wheel saturate; an absolute axis is scaled from its logical range onto +//! `0..=POINTER_ABSOLUTE_MAX`, and a value outside that range leaves the +//! position as it was (§5.10). + +use toyos_abi::input::POINTER_ABSOLUTE_MAX; +use toyos_input::keys::{Keys, Transitions, Usages}; +use toyos_input::wire::{Motion, BUTTONS}; + +use crate::descriptor::{Collection, Field, Interface, Kind, BUTTON, GENERIC_DESKTOP, KEYS, WHEEL, X, Y}; +use alloc::vec::Vec; + +const ERROR_ROLL_OVER: u16 = 0x01; +const POST_FAIL: u16 = 0x02; +const ERROR_UNDEFINED: u16 = 0x03; + +/// Why a report was not believed; nothing it carried moved anything. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Refused { + /// An empty transfer: an interface that numbers its reports sent no ID. + Empty, + /// A Report ID no collection's fields carry. + UnknownReport(u8), + /// Fewer bytes than the report's fields need. + Short { id: u8, len: usize }, + /// POSTFail in a key array: the keyboard failed its own power-on test. + PostFail, + /// ErrorUndefined in a key array: an error the keyboard does not name. + Undefined, +} + +/// What one believed report did. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Read { + /// Collection `collection`, a keyboard, made these transitions; `rolled` + /// where an array reported ErrorRollOver and only its variable fields + /// were believed. + Keys { collection: usize, made: Transitions, rolled: bool }, + /// Collection `collection`, a pointer, reported this. + Pointer { collection: usize, report: Pointer }, + /// The report is collection `collection`'s, which is not bound, and was + /// dropped. + Unbound { collection: usize }, +} + +/// A pointer report, bounded to the `input` record. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Pointer { + /// Boot-mouse order's five. + pub buttons: u8, + pub scroll: i8, + pub motion: Motion, +} + +/// What reports carried that was not delivered, for `inspect`. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +pub struct Counts { + pub reserved: u64, + pub unlisted: u64, + pub unsent_button: u64, + pub unbound: u64, +} + +/// One collection's state between its reports. +#[derive(Clone, PartialEq, Eq, Debug)] +enum State { + Keyboard(Keys), + /// The last absolute position each axis was told, which a value outside + /// its range leaves as it was. + Pointer { x: u16, y: u16 }, + Unbound, +} + +/// One HID interface, bound by its descriptor. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Hid { + interface: Interface, + states: Vec, + pub counts: Counts, +} + +impl Hid { + pub fn new(interface: Interface) -> Self { + let states = interface + .collections + .iter() + .map(|c| match c.bound { + Ok(Kind::Keyboard) => State::Keyboard(Keys::default()), + Ok(Kind::Pointer) => State::Pointer { x: 0, y: 0 }, + Err(_) => State::Unbound, + }) + .collect(); + Self { interface, states, counts: Counts::default() } + } + + pub fn collections(&self) -> &[Collection] { + &self.interface.collections + } + + /// The transitions and reports `report`, one interrupt transfer, makes. + pub fn report(&mut self, report: &[u8]) -> Result { + let (id, body) = match self.interface.numbered { + true => report.split_first().map(|(id, body)| (*id, body)).ok_or(Refused::Empty)?, + false => (0, report), + }; + let collection = self.interface.of(id).ok_or(Refused::UnknownReport(id))?; + if self.states[collection] == State::Unbound { + self.counts.unbound += 1; + return Ok(Read::Unbound { collection }); + } + let fields: Vec<&Field> = self.interface.collections[collection].fields.iter().filter(|f| f.report == id).collect(); + if fields.iter().any(|f| f.end() > body.len() as u64 * 8) { + return Err(Refused::Short { id, len: report.len() }); + } + match &mut self.states[collection] { + State::Unbound => unreachable!("answered above"), + State::Keyboard(keys) => { + let (next, rolled) = read_keys(&fields, body, keys.held(), &mut self.counts)?; + Ok(Read::Keys { collection, made: keys.hold(next), rolled }) + } + State::Pointer { x, y } => { + let report = read_pointer(&fields, body, (x, y), &mut self.counts); + Ok(Read::Pointer { collection, report }) + } + } + } + + /// The keys keyboard collection `collection` holds. + pub fn held(&self, collection: usize) -> Option { + match &self.states[collection] { + State::Keyboard(keys) => Some(keys.held()), + _ => None, + } + } + + /// Collection `collection`'s keys released: the device left. + pub fn release(&mut self, collection: usize) -> Option { + match &mut self.states[collection] { + State::Keyboard(keys) => Some(keys.hold(Usages::NONE)), + _ => None, + } + } +} + +/// What a key usage is to the owner of input. +enum Key { + Held(u8), + /// `00`, or a bitmap's `01` to `03`: nothing. + Nothing, + Reserved, +} + +fn key(usage: u16) -> Key { + match usage { + 0x00..=0x03 => Key::Nothing, + 0xA5..=0xAF | 0xDE..=0xDF | 0xE8..=0xFFFF => Key::Reserved, + usage => Key::Held(usage as u8), + } +} + +/// The keys `body` holds, from `held` before it, and whether an array rolled +/// over. +fn read_keys(fields: &[&Field], body: &[u8], held: Usages, counts: &mut Counts) -> Result<(Usages, bool), Refused> { + let (mut covered, mut down) = (Usages::NONE, Usages::NONE); + let (mut array_covered, mut array_down) = (Usages::NONE, Usages::NONE); + let mut rolled = false; + let mut reserved = 0; + let mut unlisted = 0; + for field in fields.iter().filter(|f| f.data && f.keys()) { + if field.variable { + for i in 0..field.count { + let (Some((KEYS, usage)), Some(value)) = (field.control(i), field.value(body, i)) else { continue }; + match key(usage) { + Key::Held(usage) if field.holds(value) => { + covered.insert(usage); + if value != 0 { + down.insert(usage); + } + } + Key::Reserved if value != 0 && field.holds(value) => reserved += 1, + _ => {} + } + } + continue; + } + // Every usage the array can name is released unless one of its + // controls names it now. + for index in 0..=field.logical_max.saturating_sub(field.logical_min).clamp(0, 0xFF) { + if let Some(Some((KEYS, usage))) = field.selected(field.logical_min.saturating_add(index)) { + if let Key::Held(usage) = key(usage) { + array_covered.insert(usage); + } + } + } + for i in 0..field.count { + let Some(value) = field.value(body, i) else { continue }; + match field.selected(value) { + None => {} + Some(None) => unlisted += 1, + Some(Some((KEYS, ERROR_ROLL_OVER))) => rolled = true, + Some(Some((KEYS, POST_FAIL))) => return Err(Refused::PostFail), + Some(Some((KEYS, ERROR_UNDEFINED))) => return Err(Refused::Undefined), + Some(Some((KEYS, usage))) => match key(usage) { + Key::Held(usage) => array_down.insert(usage), + Key::Reserved => reserved += 1, + Key::Nothing => {} + }, + // A usage of another page in a keyboard's array is no key. + Some(Some(_)) => {} + } + } + } + counts.reserved += reserved; + counts.unlisted += unlisted; + if !rolled { + covered = covered.union(array_covered); + down = down.union(array_down); + } + Ok((held.without(covered).union(down), rolled)) +} + +/// What `body` reports of a pointer whose last absolute position is `at`. +fn read_pointer(fields: &[&Field], body: &[u8], at: (&mut u16, &mut u16), counts: &mut Counts) -> Pointer { + let (x_at, y_at) = at; + let (mut buttons, mut scroll, mut dx, mut dy) = (0u8, 0i32, 0i32, 0i32); + let mut absolute = false; + for field in fields.iter().filter(|f| f.data) { + for i in 0..field.count { + let Some(value) = field.value(body, i) else { continue }; + let usage = if field.variable { + if !field.holds(value) { + continue; + } + field.control(i) + } else { + // An array of buttons names the one pressed. + match field.selected(value) { + Some(Some((BUTTON, usage))) => { + press(&mut buttons, usage, counts); + continue; + } + _ => continue, + } + }; + match usage { + Some((BUTTON, usage)) if value != 0 => press(&mut buttons, usage, counts), + Some((GENERIC_DESKTOP, X)) if field.relative => dx = dx.saturating_add(value), + Some((GENERIC_DESKTOP, Y)) if field.relative => dy = dy.saturating_add(value), + Some((GENERIC_DESKTOP, X)) => { + absolute = true; + *x_at = scaled(field, value); + } + Some((GENERIC_DESKTOP, Y)) => { + absolute = true; + *y_at = scaled(field, value); + } + Some((GENERIC_DESKTOP, WHEEL)) => scroll = scroll.saturating_add(value), + _ => {} + } + } + } + let saturate = |v: i32| v.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16; + let motion = match absolute || fields.iter().any(|f| f.data && !f.relative && axis(f)) { + true => Motion::Absolute { x: *x_at, y: *y_at }, + false => Motion::Relative { dx: saturate(dx), dy: saturate(dy) }, + }; + Pointer { buttons, scroll: scroll.clamp(i32::from(i8::MIN), i32::from(i8::MAX)) as i8, motion } +} + +fn axis(field: &Field) -> bool { + (0..field.count).any(|i| matches!(field.control(i), Some((GENERIC_DESKTOP, X | Y)))) +} + +fn press(buttons: &mut u8, usage: u16, counts: &mut Counts) { + match usage { + 1..=5 => *buttons |= 1 << (usage - 1) & BUTTONS, + 0 => {} + _ => counts.unsent_button += 1, + } +} + +/// `value`, inside `field`'s logical range, scaled onto +/// `0..=POINTER_ABSOLUTE_MAX`, rounded down. +fn scaled(field: &Field, value: i32) -> u16 { + let span = i64::from(field.logical_max) - i64::from(field.logical_min); + let from = i64::from(value) - i64::from(field.logical_min); + (from * i64::from(POINTER_ABSOLUTE_MAX) / span) as u16 +} diff --git a/lib/usbhid/src/tests.rs b/lib/usbhid/src/tests.rs new file mode 100644 index 00000000000..c0d6476aa73 --- /dev/null +++ b/lib/usbhid/src/tests.rs @@ -0,0 +1,654 @@ +extern crate std; + +use std::collections::BTreeSet; +use std::vec; +use std::vec::Vec; + +use toyos_input::keys::Transition; +use toyos_input::wire::Motion; + +use crate::descriptor::{self, Kind, Refused as Unread, Unbound}; +use crate::report::{Hid, Pointer, Read, Refused}; + +/// HID 1.11 Appendix E.6, the boot keyboard's descriptor: eight modifier +/// bits, a constant byte, five LED outputs and their padding, and six array +/// slots of Logical Maximum 0x65 over Usage Minimum 0 to Maximum 0x65. +const E6_KEYBOARD: &[u8] = &[ + 0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, + 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05, 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, + 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x01, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, + 0x00, 0x29, 0x65, 0x81, 0x00, 0xC0, +]; + +/// HID 1.11 Appendix E.10, the boot mouse's descriptor: three buttons, five +/// bits of padding, and X and Y as signed relative bytes. +const E10_MOUSE: &[u8] = &[ + 0x05, 0x01, 0x09, 0x02, 0xA1, 0x01, 0x09, 0x01, 0xA1, 0x00, 0x05, 0x09, 0x19, 0x01, 0x29, 0x03, 0x15, 0x00, 0x25, + 0x01, 0x95, 0x03, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75, 0x05, 0x81, 0x01, 0x05, 0x01, 0x09, 0x30, 0x09, 0x31, + 0x15, 0x81, 0x25, 0x7F, 0x75, 0x08, 0x95, 0x02, 0x81, 0x06, 0xC0, 0xC0, +]; + +fn hid(descriptor: &[u8]) -> Hid { + Hid::new(descriptor::parse(descriptor).expect("a descriptor this reader reads")) +} + +fn press(usage: u8) -> Transition { + Transition { usage, pressed: true } +} + +fn lift(usage: u8) -> Transition { + Transition { usage, pressed: false } +} + +/// What a keyboard report made, collected: `Ok((transitions, rolled))`. +fn keys(hid: &mut Hid, report: &[u8]) -> Result<(Vec, bool), Refused> { + match hid.report(report)? { + Read::Keys { made, rolled, .. } => Ok((made.collect(), rolled)), + other => panic!("{report:02x?} read as {other:?}"), + } +} + +/// A report that named every key down. +fn fed(hid: &mut Hid, report: &[u8]) -> Vec { + match keys(hid, report) { + Ok((made, false)) => made, + other => panic!("{report:02x?}: {other:?}"), + } +} + +fn pointer(hid: &mut Hid, report: &[u8]) -> Pointer { + match hid.report(report) { + Ok(Read::Pointer { report, .. }) => report, + other => panic!("{report:02x?} read as {other:?}"), + } +} + +/// Descriptor items, spelled. +mod item { + pub fn page(p: u8) -> [u8; 2] { + [0x05, p] + } + pub fn usage(u: u8) -> [u8; 2] { + [0x09, u] + } + pub fn usage_min(u: u8) -> [u8; 2] { + [0x19, u] + } + pub fn usage_max(u: u8) -> [u8; 2] { + [0x29, u] + } + pub fn logical_min(v: i8) -> [u8; 2] { + [0x15, v as u8] + } + pub fn logical_max(v: u8) -> [u8; 2] { + [0x25, v] + } + pub fn logical_min16(v: i16) -> [u8; 3] { + let [a, b] = v.to_le_bytes(); + [0x16, a, b] + } + pub fn logical_max16(v: i16) -> [u8; 3] { + let [a, b] = v.to_le_bytes(); + [0x26, a, b] + } + pub fn size(bits: u8) -> [u8; 2] { + [0x75, bits] + } + pub fn count(n: u8) -> [u8; 2] { + [0x95, n] + } + pub fn id(id: u8) -> [u8; 2] { + [0x85, id] + } + pub const APPLICATION: [u8; 2] = [0xA1, 0x01]; + pub const END: [u8; 1] = [0xC0]; + pub const VARIABLE: [u8; 2] = [0x81, 0x02]; + pub const RELATIVE: [u8; 2] = [0x81, 0x06]; + pub const ARRAY: [u8; 2] = [0x81, 0x00]; + pub const CONSTANT: [u8; 2] = [0x81, 0x01]; +} + +fn spell(items: &[&[u8]]) -> Vec { + items.concat() +} + +#[test] +fn the_boot_descriptors_bind_a_keyboard_and_a_pointer() { + let keyboard = descriptor::parse(E6_KEYBOARD).unwrap(); + assert_eq!(keyboard.collections.len(), 1); + assert_eq!(keyboard.collections[0].bound, Ok(Kind::Keyboard)); + assert!(!keyboard.numbered); + let mouse = descriptor::parse(E10_MOUSE).unwrap(); + assert_eq!(mouse.collections.len(), 1); + assert_eq!(mouse.collections[0].bound, Ok(Kind::Pointer)); +} + +/// The boot keyboard's report byte for byte, through its descriptor: bit n +/// of byte 0 is usage 0xE0 + n, byte 1 is constant, and each slot names its +/// usage. +#[test] +fn a_keyboard_report_is_read_as_its_descriptor_lays_it_out() { + for n in 0..8u8 { + let mut kb = hid(E6_KEYBOARD); + assert_eq!(fed(&mut kb, &[1 << n, 0, 0, 0, 0, 0, 0, 0]), vec![press(0xE0 + n)]); + } + let mut kb = hid(E6_KEYBOARD); + assert_eq!(fed(&mut kb, &[0, 0xFF, 0, 0, 0, 0, 0, 0]), vec![], "byte 1 is constant"); + for slot in 2..8 { + let mut kb = hid(E6_KEYBOARD); + let mut report = [0u8; 8]; + report[slot] = 0x04; + assert_eq!(fed(&mut kb, &report), vec![press(0x04)], "slot {slot}"); + } +} + +/// Shift before the key it shifts, and a release before the press that took +/// its slot. +#[test] +fn modifiers_come_first_then_releases_then_presses() { + let mut kb = hid(E6_KEYBOARD); + assert_eq!(fed(&mut kb, &[0x02, 0, 0x05, 0, 0, 0, 0, 0]), vec![press(0xE1), press(0x05)]); + assert_eq!(fed(&mut kb, &[0x00, 0, 0x06, 0, 0, 0, 0, 0]), vec![lift(0xE1), lift(0x05), press(0x06)]); + assert_eq!(fed(&mut kb, &[0x05, 0, 0x07, 0, 0, 0, 0, 0]), vec![press(0xE0), press(0xE2), lift(0x06), press(0x07)]); + assert_eq!(fed(&mut kb, &[0x05, 0, 0x07, 0, 0, 0, 0, 0]), vec![], "nothing changed"); +} + +/// A key that moves slot is not a release and a press; a usage held twice is +/// held once. +#[test] +fn a_report_is_a_set_of_keys_and_not_a_list_of_slots() { + let mut kb = hid(E6_KEYBOARD); + assert_eq!(fed(&mut kb, &[0, 0, 0x04, 0x05, 0, 0, 0, 0]), vec![press(0x04), press(0x05)]); + assert_eq!(fed(&mut kb, &[0, 0, 0x05, 0x04, 0, 0, 0, 0]), vec![]); + assert_eq!(fed(&mut kb, &[0, 0, 0x05, 0x05, 0x04, 0x05, 0, 0]), vec![]); + assert_eq!(fed(&mut kb, &[0x01, 0, 0, 0, 0, 0, 0, 0]), vec![press(0xE0), lift(0x04), lift(0x05)]); + assert_eq!(fed(&mut kb, &[0; 8]), vec![lift(0xE0)]); +} + +/// POSTFail and ErrorUndefined in any slot refuse the report, and the keys +/// held before are still held. +#[test] +fn a_fault_usage_is_refused_and_the_keys_held_stay_held() { + for (usage, why) in [(2, Refused::PostFail), (3, Refused::Undefined)] { + for slot in 2..8 { + for beside in [0x04, 1] { + let mut kb = hid(E6_KEYBOARD); + fed(&mut kb, &[0x02, 0, 0x04, 0x05, 0, 0, 0, 0]); + let before = kb.clone(); + let mut report = [0x00, 0, beside, beside, beside, beside, beside, beside]; + report[slot] = usage; + assert_eq!(keys(&mut kb, &report), Err(why), "usage {usage} in slot {slot}"); + assert_eq!(kb, before, "a refused report changed what is held"); + } + } + } +} + +/// Appendix C's phantom state in any slot: the array is not believed, so the +/// keys it held stay held, and the modifier bitmap is, so a modifier moves. +#[test] +fn a_rollover_report_believes_its_modifiers_and_not_its_slots() { + for slot in 2..8 { + let mut kb = hid(E6_KEYBOARD); + fed(&mut kb, &[0x02, 0, 0x04, 0x05, 0, 0, 0, 0]); + let mut report = [0x00, 0, 0x06, 0, 0, 0, 0, 0]; + report[slot] = 1; + assert_eq!(keys(&mut kb, &report), Ok((vec![lift(0xE1)], true)), "rollover in slot {slot}"); + assert_eq!(fed(&mut kb, &[0, 0, 0x04, 0x05, 0, 0, 0, 0]), vec![], "slot {slot}"); + } + let mut kb = hid(E6_KEYBOARD); + fed(&mut kb, &[0x02, 0, 0x04, 0x05, 0, 0, 0, 0]); + assert_eq!(keys(&mut kb, &[0x00, 0, 1, 1, 1, 1, 1, 1]), Ok((vec![lift(0xE1)], true))); + assert_eq!(keys(&mut kb, &[0x01, 0, 1, 1, 1, 1, 1, 1]), Ok((vec![press(0xE0)], true))); + assert_eq!(keys(&mut kb, &[0x00, 0, 1, 1, 1, 1, 1, 1]), Ok((vec![lift(0xE0)], true))); + assert_eq!( + fed(&mut kb, &[0x00, 0, 0x05, 0, 0, 0, 0, 0]), + vec![lift(0x04)], + "the next whole report is diffed against the keys held through the rollover" + ); +} + +/// A report shorter than its fields is refused; a longer one is read for the +/// fields it holds. +#[test] +fn a_report_shorter_than_its_descriptor_is_refused() { + for len in 0..8 { + let mut kb = hid(E6_KEYBOARD); + fed(&mut kb, &[0, 0, 0x04, 0, 0, 0, 0, 0]); + let before = kb.clone(); + assert_eq!(keys(&mut kb, &vec![0x02; len]), Err(Refused::Short { id: 0, len })); + assert_eq!(kb, before); + } + let mut kb = hid(E6_KEYBOARD); + assert_eq!(fed(&mut kb, &[0, 0, 0x04, 0, 0, 0, 0, 0, 0xFF]), vec![press(0x04)]); +} + +#[test] +fn a_keyboard_leaving_releases_everything_it_held() { + let mut kb = hid(E6_KEYBOARD); + fed(&mut kb, &[0x81, 0xFF, 0x04, 0x65, 0, 0, 0, 0]); + let released: Vec<_> = kb.release(0).unwrap().collect(); + assert_eq!(released, vec![lift(0xE0), lift(0xE7), lift(0x04), lift(0x65)]); + assert_eq!(kb.release(0).unwrap().count(), 0); +} + +/// Every report QEMU 11.1's `usb-kbd` delivered over xHCI for QMP key +/// events, in the boot layout its descriptor shares: `a` down and up, +/// Shift+B, then Q W E R T Y U down, U up, and the rest up. The seventh key +/// is ErrorRollOver in every slot. +#[test] +fn reports_qemu_sent_read_as_the_keys_that_were_pressed() { + let mut kb = hid(E6_KEYBOARD); + let captured: &[(&[u8], Vec, bool)] = &[ + (&[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00], vec![press(0x04)], false), + (&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], vec![lift(0x04)], false), + (&[0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], vec![press(0xE1)], false), + (&[0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00], vec![press(0x05)], false), + (&[0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], vec![lift(0x05)], false), + (&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], vec![lift(0xE1)], false), + (&[0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00], vec![press(0x14)], false), + (&[0x00, 0x00, 0x14, 0x1A, 0x00, 0x00, 0x00, 0x00], vec![press(0x1A)], false), + (&[0x00, 0x00, 0x14, 0x1A, 0x08, 0x00, 0x00, 0x00], vec![press(0x08)], false), + (&[0x00, 0x00, 0x14, 0x1A, 0x08, 0x15, 0x00, 0x00], vec![press(0x15)], false), + (&[0x00, 0x00, 0x14, 0x1A, 0x08, 0x15, 0x17, 0x00], vec![press(0x17)], false), + (&[0x00, 0x00, 0x14, 0x1A, 0x08, 0x15, 0x17, 0x1C], vec![press(0x1C)], false), + (&[0x00, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01], vec![], true), + (&[0x00, 0x00, 0x14, 0x1A, 0x08, 0x15, 0x17, 0x1C], vec![], false), + (&[0x00, 0x00, 0x1C, 0x1A, 0x08, 0x15, 0x17, 0x00], vec![lift(0x14)], false), + (&[0x00, 0x00, 0x1C, 0x17, 0x08, 0x15, 0x00, 0x00], vec![lift(0x1A)], false), + (&[0x00, 0x00, 0x1C, 0x17, 0x15, 0x00, 0x00, 0x00], vec![lift(0x08)], false), + (&[0x00, 0x00, 0x1C, 0x17, 0x00, 0x00, 0x00, 0x00], vec![lift(0x15)], false), + (&[0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00], vec![lift(0x17)], false), + (&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], vec![lift(0x1C)], false), + ]; + for (at, (report, made, rolled)) in captured.iter().enumerate() { + assert_eq!(keys(&mut kb, report), Ok((made.clone(), *rolled)), "report {at}: {report:02x?}"); + } +} + +/// §6.2.2.5: an array reports indexes. With Logical Minimum 1 against Usage +/// Minimum 0x04, index 1 is `a` (0x04) and index 0x62 is usage 0x65; index 0 +/// and index 0x63 are outside the logical range and assert nothing. +#[test] +fn an_array_whose_usage_minimum_is_not_its_logical_minimum_reads_indexes() { + use item::*; + let d = spell(&[ + &page(1), &usage(6), &APPLICATION, &page(7), &size(8), &count(2), + &logical_min(1), &logical_max(0x62), &usage_min(0x04), &usage_max(0x65), &ARRAY, &END, + ]); + let mut kb = hid(&d); + assert_eq!(fed(&mut kb, &[1, 0]), vec![press(0x04)], "index 1 is usage 0x04"); + assert_eq!(fed(&mut kb, &[1, 0x62]), vec![press(0x65)], "index 0x62 is usage 0x65"); + assert_eq!(fed(&mut kb, &[0, 0x63]), vec![lift(0x04), lift(0x65)], "indexes outside the range assert nothing"); + assert_eq!(fed(&mut kb, &[0x04, 0]), vec![press(0x07)], "index 4 is usage 0x07, not 0x04"); +} + +/// A Keyboard/Keypad variable field is a bitmap of usages: a bit set is that +/// usage held, and cleared released. +#[test] +fn a_bitmap_keyboard_is_bound_and_each_bit_is_its_usage() { + use item::*; + // The modifier byte, then one bit for each of 0x04..=0x65 and two bits + // of padding to a byte. + let d = spell(&[ + &page(1), &usage(6), &APPLICATION, &page(7), &logical_min(0), &logical_max(1), &size(1), + &count(8), &usage_min(0xE0), &usage_max(0xE7), &VARIABLE, + &count(0x62), &usage_min(0x04), &usage_max(0x65), &VARIABLE, + &count(2), &CONSTANT, &END, + ]); + let interface = descriptor::parse(&d).unwrap(); + assert_eq!(interface.collections[0].bound, Ok(Kind::Keyboard)); + let mut kb = Hid::new(interface); + let bit = |usage: u8| -> Vec { + let mut r = vec![0u8; 14]; + let at = 8 + usize::from(usage - 0x04); + r[at / 8] |= 1 << (at % 8); + r + }; + assert_eq!(fed(&mut kb, &bit(0x04)), vec![press(0x04)]); + assert_eq!(fed(&mut kb, &bit(0x65)), vec![lift(0x04), press(0x65)]); + let mut shifted = bit(0x1D); + shifted[0] = 0x02; + assert_eq!(fed(&mut kb, &shifted), vec![press(0xE1), lift(0x65), press(0x1D)]); + assert_eq!(fed(&mut kb, &[0; 14]), vec![lift(0xE1), lift(0x1D)]); +} + +/// A bitmap from Usage Minimum 0 has controls at `00` to `03`, which are no +/// physical keys (HUT 1.5 p.96): their bits press nothing, and ErrorRollOver +/// never reaches the owner as a press. +#[test] +fn a_bitmaps_controls_at_00_to_03_press_nothing() { + use item::*; + let d = spell(&[ + &page(1), &usage(6), &APPLICATION, &page(7), &logical_min(0), &logical_max(1), &size(1), + &count(8), &usage_min(0x00), &usage_max(0x07), &VARIABLE, &END, + ]); + let mut kb = hid(&d); + assert_eq!(fed(&mut kb, &[0x0F]), vec![]); + assert_eq!(fed(&mut kb, &[0x1F]), vec![press(0x04)]); +} + +/// HUT 1.5 reserves `A5-AF`, `DE-DF` and `E8-FFFF`: each reached by an array's +/// index or a bitmap's bit is counted and not delivered, while the usage +/// beside each range is; an idle array's 00 slots assert nothing and count +/// nothing. +#[test] +fn a_reserved_usage_names_no_key_through_an_array_or_a_bitmap() { + use item::*; + let reserved = [0xA5, 0xAF, 0xDE, 0xDF, 0xE8]; + let beside = [0xA4, 0xB0, 0xDD, 0xE7]; + // An array over the whole page, index equal to usage. + let array = spell(&[ + &page(1), &usage(6), &APPLICATION, &page(7), &size(8), &count(2), &logical_min(0), + &logical_max(0xFF), &usage_min(0), &usage_max(0xFF), &ARRAY, &END, + ]); + for usage in reserved { + let mut kb = hid(&array); + assert_eq!(fed(&mut kb, &[usage, 0]), vec![], "{usage:#04x} through an array"); + assert_eq!(kb.counts.reserved, 1, "{usage:#04x}"); + } + for usage in beside { + let mut kb = hid(&array); + assert_eq!(fed(&mut kb, &[usage, 0]), vec![press(usage)], "{usage:#04x} through an array"); + assert_eq!(kb.counts.reserved, 0); + } + let mut kb = hid(&array); + assert_eq!(fed(&mut kb, &[0, 0]), vec![]); + assert_eq!(kb.counts, crate::report::Counts::default(), "an idle array counts nothing"); + + // A bitmap over the whole page: 256 bits. + let bitmap = spell(&[ + &page(1), &usage(6), &APPLICATION, &page(7), &logical_min(0), &logical_max(1), &size(1), + &[0x96, 0x00, 0x01], &usage_min(0), &[0x2A, 0xFF, 0x00], &VARIABLE, &END, + ]); + let bit = |usage: u8| -> Vec { + let mut r = vec![0u8; 32]; + r[usize::from(usage / 8)] |= 1 << (usage % 8); + r + }; + for usage in reserved { + let mut kb = hid(&bitmap); + assert_eq!(fed(&mut kb, &bit(usage)), vec![], "{usage:#04x} through a bitmap"); + assert_eq!(kb.counts.reserved, 1, "{usage:#04x}"); + } + for usage in beside { + let mut kb = hid(&bitmap); + assert_eq!(fed(&mut kb, &bit(usage)), vec![press(usage)], "{usage:#04x} through a bitmap"); + } +} + +/// An array index whose entry is past the field's usage list names no key. +#[test] +fn an_index_past_the_usage_list_is_counted_unlisted() { + use item::*; + let d = spell(&[ + &page(1), &usage(6), &APPLICATION, &page(7), &size(8), &count(1), &logical_min(0), + &logical_max(10), &usage(0x04), &usage(0x05), &ARRAY, &END, + ]); + let mut kb = hid(&d); + assert_eq!(fed(&mut kb, &[1]), vec![press(0x05)]); + assert_eq!(fed(&mut kb, &[2]), vec![lift(0x05)]); + assert_eq!(kb.counts.unlisted, 1); +} + +/// A keyboard and a Consumer Control collection under two Report IDs: the +/// keyboard's reports are delivered and the other's counted and dropped. +#[test] +fn a_collection_this_reader_cannot_bind_costs_only_its_own_reports() { + use item::*; + let d = spell(&[ + &page(1), &usage(6), &APPLICATION, &id(1), &page(7), &size(8), &count(1), &logical_min(0), + &logical_max(0x65), &usage_min(0), &usage_max(0x65), &ARRAY, &END, + &page(0x0C), &usage(0x01), &APPLICATION, &id(2), &logical_min(0), &[0x26, 0xFF, 0x03], &size(16), + &count(1), &usage_min(0), &[0x2A, 0xFF, 0x03], &ARRAY, &END, + ]); + let interface = descriptor::parse(&d).unwrap(); + assert_eq!(interface.collections[0].bound, Ok(Kind::Keyboard)); + assert_eq!(interface.collections[1].bound, Err(Unbound::NotAKeyboardOrPointer { page: 0x0C, usage: 0x01 })); + let mut kb = Hid::new(interface); + assert_eq!(fed(&mut kb, &[1, 0x04]), vec![press(0x04)]); + assert_eq!(kb.report(&[2, 0xE9, 0x00]), Ok(Read::Unbound { collection: 1 })); + assert_eq!(kb.counts.unbound, 1); + assert_eq!(fed(&mut kb, &[1, 0x00]), vec![lift(0x04)]); + assert_eq!(kb.report(&[3, 0]), Err(Refused::UnknownReport(3))); +} + +/// §8.4: a Report ID in two top-level collections refuses the interface, as +/// binding one half would read the other's fields as absent. +#[test] +fn a_report_id_spanning_two_collections_refuses_the_interface() { + use item::*; + let d = spell(&[ + &page(1), &usage(6), &APPLICATION, &id(1), &page(7), &size(8), &count(1), &logical_min(0), + &logical_max(0x65), &usage_min(0), &usage_max(0x65), &ARRAY, &END, + &page(1), &usage(2), &APPLICATION, &id(1), &page(1), &usage(0x30), &logical_min(-127), + &logical_max(127), &size(8), &count(1), &RELATIVE, &END, + ]); + assert_eq!(descriptor::parse(&d), Err(Unread::SpansCollections { id: 1 })); +} + +/// Button page usages 1 to 8: button 6 pressed is counted and not sent, and +/// button 1 in the same report is. +#[test] +fn a_button_past_the_fifth_is_counted_and_the_rest_delivered() { + use item::*; + let d = spell(&[ + &page(1), &usage(2), &APPLICATION, &page(9), &usage_min(1), &usage_max(8), &logical_min(0), + &logical_max(1), &size(1), &count(8), &VARIABLE, &page(1), &usage(0x30), &usage(0x31), + &logical_min(-127), &logical_max(127), &size(8), &count(2), &RELATIVE, &END, + ]); + let mut mouse = hid(&d); + let read = pointer(&mut mouse, &[0b0010_0001, 3, 0xFD]); + assert_eq!(read, Pointer { buttons: 0x01, scroll: 0, motion: Motion::Relative { dx: 3, dy: -3 } }); + assert_eq!(mouse.counts.unsent_button, 1); +} + +/// An absolute axis of Logical Minimum −100 to Maximum 100 is scaled onto the +/// record's range, and a value outside it leaves the position as it was. +#[test] +fn an_absolute_axis_is_scaled_onto_the_records_range() { + use item::*; + let d = spell(&[ + &page(1), &usage(2), &APPLICATION, &page(1), &usage(0x30), &usage(0x31), &logical_min16(-100), + &logical_max16(100), &size(16), &count(2), &VARIABLE, &END, + ]); + let mut tablet = hid(&d); + let at = |x: i16, y: i16| -> Vec { [x.to_le_bytes(), y.to_le_bytes()].concat() }; + let abs = |x, y| Pointer { buttons: 0, scroll: 0, motion: Motion::Absolute { x, y } }; + assert_eq!(pointer(&mut tablet, &at(-100, 100)), abs(0, 32767)); + assert_eq!(pointer(&mut tablet, &at(0, -100)), abs(16383, 0)); + assert_eq!(pointer(&mut tablet, &at(101, 100)), abs(16383, 32767), "101 leaves X as it was"); +} + +/// The wheel is the Generic Desktop Wheel usage wherever the descriptor puts +/// it: here before the axes. +#[test] +fn the_wheel_is_where_the_descriptor_puts_it() { + use item::*; + let d = spell(&[ + &page(1), &usage(2), &APPLICATION, &page(1), &usage(0x38), &usage(0x30), &usage(0x31), + &logical_min(-127), &logical_max(127), &size(8), &count(3), &RELATIVE, &END, + ]); + let mut mouse = hid(&d); + assert_eq!(pointer(&mut mouse, &[0xFF, 2, 3]), Pointer { buttons: 0, scroll: -1, motion: Motion::Relative { dx: 2, dy: 3 } }); +} + +/// The boot mouse through its descriptor, with the reports QEMU 11.1's +/// `usb-mouse` delivered over xHCI for QMP moves of (5, −5) and (−200, 200), +/// which QEMU splits at a byte's range, and the left button down and up. +#[test] +fn a_mouse_report_is_read_as_its_descriptor_lays_it_out() { + let mut mouse = hid(E10_MOUSE); + let rel = |buttons, dx, dy| Pointer { buttons, scroll: 0, motion: Motion::Relative { dx, dy } }; + assert_eq!(pointer(&mut mouse, &[0x00, 0x05, 0xFB, 0x00]), rel(0, 5, -5)); + assert_eq!(pointer(&mut mouse, &[0x00, 0x81, 0x7F, 0x00]), rel(0, -127, 127)); + assert_eq!(pointer(&mut mouse, &[0x00, 0xB7, 0x49, 0x00]), rel(0, -73, 73)); + assert_eq!(pointer(&mut mouse, &[0x01, 0x00, 0x00, 0x00]), rel(1, 0, 0)); + assert_eq!(pointer(&mut mouse, &[0x00, 0x00, 0x00]), rel(0, 0, 0)); + assert_eq!(pointer(&mut mouse, &[0xFF, 0x01, 0x00]), rel(7, 1, 0), "the padding bits are not buttons"); + assert_eq!(pointer(&mut mouse, &[0x00, 0x80, 0x01]), rel(0, 0, 1), "-128 is outside −127..=127 and moves nothing"); +} + +#[test] +fn what_breaks_the_reading_refuses_the_interface_by_name() { + use item::*; + let cases: &[(Vec, Unread)] = &[ + (vec![0x05], Unread::Truncated { at: 0 }), + (vec![0xA1, 0x01, 0x27, 0x00], Unread::Truncated { at: 2 }), + (vec![0xC0], Unread::Unbalanced { at: 0 }), + (spell(&[&page(1), &usage(6), &APPLICATION]), Unread::Unbalanced { at: 6 }), + (spell(&[&size(8), &count(1), &VARIABLE]), Unread::Outside { at: 4 }), + (spell(&[&id(0)]), Unread::ReportId { at: 0 }), + (vec![0xB4], Unread::GlobalStack { at: 0 }), + (vec![0xA4, 0xA4, 0xA4, 0xA4, 0xA4], Unread::GlobalStack { at: 4 }), + ( + spell(&[&APPLICATION, &size(8), &count(1), &VARIABLE, &id(1), &VARIABLE, &END]), + Unread::Unnumbered, + ), + ]; + for (d, why) in cases { + assert_eq!(descriptor::parse(d), Err(*why), "{d:02x?}"); + } + let deep: Vec = [APPLICATION; 9].concat(); + assert_eq!(descriptor::parse(&deep), Err(Unread::TooDeep { at: 16 })); +} + +/// A long item is stepped over: no tag is defined for one (§6.2.2.3). +#[test] +fn a_long_item_is_stepped_over() { + let mut d = vec![0xFE, 0x02, 0x10, 0xAA, 0xBB]; + d.extend_from_slice(E6_KEYBOARD); + assert_eq!(descriptor::parse(&d).unwrap().collections[0].bound, Ok(Kind::Keyboard)); + assert_eq!(descriptor::parse(&[0xFE, 0x05, 0x10, 0xAA]), Err(Unread::Truncated { at: 0 })); +} + +#[test] +fn a_collection_whose_report_size_or_length_this_reader_cannot_hold_is_not_bound() { + use item::*; + let wide = spell(&[&page(1), &usage(6), &APPLICATION, &page(7), &size(33), &count(1), &VARIABLE, &END]); + assert_eq!(descriptor::parse(&wide).unwrap().collections[0].bound, Err(Unbound::ReportSize { bits: 33 })); + let long = spell(&[ + &page(1), &usage(6), &APPLICATION, &page(7), &logical_min(0), &logical_max(0x65), &usage_min(0), + &usage_max(0x65), &size(8), &count(65), &ARRAY, &END, + ]); + assert_eq!(descriptor::parse(&long).unwrap().collections[0].bound, Err(Unbound::ReportTooLong { id: 0 })); + let empty = spell(&[ + &page(1), &usage(2), &APPLICATION, &usage(0x30), &logical_min(0), &logical_max(0), &size(8), &count(1), + &VARIABLE, &END, + ]); + assert_eq!( + descriptor::parse(&empty).unwrap().collections[0].bound, + Err(Unbound::EmptyRange { page: 1, usage: 0x30 }) + ); +} + +/// xorshift64, seeded per test so a failure names the case that made it. +struct Bytes(u64); + +impl Bytes { + fn next(&mut self) -> u64 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } + + fn byte(&mut self, edges: &[u8]) -> u8 { + let roll = self.next(); + if roll & 1 == 0 { edges[(roll >> 1) as usize % edges.len()] } else { (roll >> 8) as u8 } + } +} + +/// What an E.6 report reads as, written from Appendices B.1, C and E.6 and not +/// as [`Hid`] does: `None` is a refusal; otherwise the set held after it and +/// whether it rolled over. +fn read_by(report: &[u8], held: &BTreeSet) -> Option<(BTreeSet, bool)> { + let slots = report.get(2..8).filter(|_| report.len() >= 8)?; + if slots.iter().any(|&u| u == 2 || u == 3) { + return None; + } + let modifiers = (0..8).filter(|n| report[0] & (1 << n) != 0).map(|n| 0xE0 + n); + if slots.contains(&1) { + let kept = held.iter().copied().filter(|u| !(0xE0..=0xE7).contains(u)); + return Some((kept.chain(modifiers).collect(), true)); + } + let named = slots.iter().copied().filter(|&u| (4..=0x65).contains(&u)); + Some((modifiers.chain(named).collect(), false)) +} + +/// Every report of any length and bytes is believed or refused by name; a +/// believed one moves the keyboard to exactly the set the oracle says, one +/// transition per usage that changed, and a refused one moves nothing. +#[test] +fn every_keyboard_report_is_believed_or_refused_and_nothing_else() { + const EDGES: &[u8] = &[0, 1, 2, 3, 4, 0x39, 0x65, 0x66, 0xDF, 0xE0, 0xE3, 0xE7, 0xE8, 0xFF]; + let mut bytes = Bytes(0x7573_6268_6964); + let mut kb = hid(E6_KEYBOARD); + let mut held = BTreeSet::new(); + let (mut whole, mut rolled, mut refused) = (0, 0, 0); + for case in 0..200_000 { + let len = if bytes.next().is_multiple_of(8) { (bytes.next() % 12) as usize } else { 8 }; + let report: Vec = (0..len).map(|_| bytes.byte(EDGES)).collect(); + let before = kb.clone(); + match (keys(&mut kb, &report), read_by(&report, &held)) { + (Ok((made, r)), Some((now, rolled_over))) => { + assert_eq!(r, rolled_over, "case {case}: {report:02x?}"); + if r { rolled += 1 } else { whole += 1 } + let changed: BTreeSet = held.symmetric_difference(&now).copied().collect(); + let named: BTreeSet = made.iter().map(|t| t.usage).collect(); + assert_eq!(named.len(), made.len(), "case {case}: a usage twice in {made:?}"); + assert_eq!(named, changed, "case {case}: {report:02x?}"); + assert!(made.iter().all(|t| t.pressed == now.contains(&t.usage)), "case {case}"); + held = now; + } + (Err(why), None) => { + refused += 1; + assert_eq!(kb, before, "case {case}: {why:?} moved the keyboard"); + } + (read, want) => panic!("case {case}: {report:02x?} read as {read:?}, the oracle says {want:?}"), + } + } + assert!(whole > 10_000 && rolled > 10_000 && refused > 10_000, "{whole} whole, {rolled} rolled, {refused} refused"); +} + +/// Any descriptor bytes are read or refused, and every report through any +/// descriptor read is believed or refused: nothing panics, and a pointer +/// report never leaves the record's range. +#[test] +fn no_descriptor_or_report_panics() { + // Item prefixes the parser acts on, so random descriptors reach past + // their first byte. + const ITEMS: &[u8] = &[ + 0x05, 0x09, 0x15, 0x16, 0x19, 0x25, 0x26, 0x29, 0x75, 0x85, 0x95, 0x81, 0x91, 0xA1, 0xC0, 0xA4, 0xB4, 0xFE, + 0x0B, 0x27, 0x17, 0x2A, 0x1A, + ]; + let mut bytes = Bytes(0x6465_7363_7269); + let mut read = 0; + for case in 0..20_000 { + let mut d: Vec = Vec::new(); + match case % 3 { + 0 => d.extend_from_slice(E6_KEYBOARD), + 1 => d.extend_from_slice(E10_MOUSE), + _ => {} + } + for _ in 0..(bytes.next() % 6) { + let at = (bytes.next() as usize) % (d.len() + 1); + d.insert(at, bytes.byte(ITEMS)); + d.insert(at + 1, bytes.byte(&[0, 1, 2, 6, 7, 8, 0x30, 0x31, 0x38, 0x7F, 0x80, 0xFF])); + } + let Ok(interface) = descriptor::parse(&d) else { continue }; + read += 1; + let mut device = Hid::new(interface); + for _ in 0..32 { + let len = (bytes.next() % 12) as usize; + let report: Vec = (0..len).map(|_| bytes.byte(&[0, 1, 2, 3, 0x7F, 0x80, 0xFF])).collect(); + if let Ok(Read::Pointer { report: p, .. }) = device.report(&report) { + assert_eq!(p.buttons & !0x1F, 0, "case {case}"); + if let Motion::Absolute { x, y } = p.motion { + assert!(x <= 32767 && y <= 32767, "case {case}: ({x}, {y})"); + } + } + } + } + assert!(read > 5_000, "{read} descriptors read"); +} diff --git a/lib/xhci/Cargo.toml b/lib/xhci/Cargo.toml deleted file mode 100644 index 88f00d497d9..00000000000 --- a/lib/xhci/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -# This crate and `sim/` are two members of the host workspace (root -# `Cargo.toml`), which is where both are listed. They used to be a workspace of -# their own here; the set of host-tested crates is one list now, and it is that -# one. - -[package] -name = "toyos-xhci" -description = "The xHCI root-hub port machine, as a decision separated from its effects." -version = "0.1.0" -edition = "2021" -license = "MIT OR Apache-2.0" - -[features] -# Host-side additions for the simulator and its tests. Never enabled by the -# kernel. -std = [] -# Compiles the deliberate defects the negative gates need, and the field that -# selects one. The kernel does not enable it, so no production path can reach a -# flawed decision — the field does not exist there. -flaws = [] - -[dependencies] -# The command a Bulk-Only wrapper carries. -toyos-scsi = { path = "../scsi" } diff --git a/lib/xhci/sim/Cargo.toml b/lib/xhci/sim/Cargo.toml deleted file mode 100644 index 64873765858..00000000000 --- a/lib/xhci/sim/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "toyos-xhci-sim" -description = "The host simulator for the xHCI port machine." -version = "0.1.0" -edition = "2021" -license = "MIT OR Apache-2.0" - -[dependencies] -# `flaws` compiles the deliberate defects the negative gates need. The kernel -# does not enable it, so no production path can reach one. -toyos-xhci = { path = "..", features = ["std", "flaws"] } -rand = { version = "0.9", features = ["small_rng"] } - -[lib] -name = "toyos_xhci_sim" -path = "src/lib.rs" diff --git a/lib/xhci/sim/src/driver.rs b/lib/xhci/sim/src/driver.rs deleted file mode 100644 index 0f19ebddad0..00000000000 --- a/lib/xhci/sim/src/driver.rs +++ /dev/null @@ -1,701 +0,0 @@ -//! The loop the kernel runs, with the effects replaced by a record of them. -//! -//! Deliberately the *shape* the kernel takes and not a convenience: drain the -//! controller's answers, act on whatever is finished, and where -//! [`port::due`] says so read the register, ask the machine, do the one thing -//! it says, read again. A simulator whose loop differs -//! from the driver's tests a driver nobody ships. -//! -//! **Nothing here can wait**, and that is the property under test as much as any -//! assertion: there is no primitive in this file that spins on a clock, so a -//! teardown or a recovery that could only be expressed by waiting could not be -//! written here at all. - -use core::num::NonZeroU8; - -use toyos_xhci::enumerate::{self, Enumeration, Learnt, Next, Request}; -use toyos_xhci::invariants::{self, Violation}; -use toyos_xhci::job::{Await, Outstanding, Stages, CC_SUCCESS}; -use toyos_xhci::port::{self, Flaw, GaveUp, Gone, Nanos, PortState, Reset, Step}; -use toyos_xhci::recovery::{Act, EndpointState, NeedsConfigure, Recovery}; -use toyos_xhci::Protocol; - -use crate::hub::FakePort; - -/// An effect the driver performed on the *port*, in the order it performed -/// them. The slot, the pool block and the endpoint are asked about separately, -/// because they move on the controller's clock rather than the port's. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Did { - Enumerated { slot: Option, trained: bool }, - Reset(Reset), - ToreDown(Gone), - GaveUp(GaveUp), -} - -/// Why a pump stopped without the port going quiet. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Stuck { - /// An invariant of the port machine did not hold. - Broke(Violation), - /// An invariant of the teardown-and-recovery loop did not hold. - Order(Broke), - /// The machine kept asking for work without ever going idle or waiting for - /// a future instant. A live-lock is a failure whatever it looks like from - /// inside. - NoProgress, - /// A pass left the port with work of its own and no instant to come back - /// at, so the kernel would not step it again until some other event. - Unwoken, -} - -/// What the loop must never do, whatever sequence produced it. -/// -/// Checked here rather than in [`invariants`] because the resources these are -/// about — the controller's slot and the driver's pool block — belong to the -/// driver and not to the port machine, which knows only what a register reads. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Broke { - /// A port was enumerated or torn down while the controller still owed an - /// answer about the last thing it was given. The Enable Slot that follows - /// may be handed the very slot whose Disable Slot has not completed, and - /// the driver is then about to zero the device-context pointer the new - /// device is using. - ActedWithAnAnswerOutstanding, - /// A pool block went back to the pool while the controller still owed an - /// answer about the slot whose endpoint contexts name that memory. - ReleasedABlockWithItsSlotOutstanding, -} - -/// How the controller answers the commands it is given. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Answers { - /// Success, this long after the command was submitted. - After(Nanos), - /// Nothing ever comes back. A controller wedged by the device that left, - /// which is the state no `device_del` can stage and the reason the deadline - /// exists at all. - Never, - /// It answers, with a completion code that is not Success. - Refuses(u32), -} - - -/// How long the simulated driver waits for an answer before calling the -/// controller silent. It stands for the kernel's own transfer budget and is not -/// read from it — nothing here is testing what that number *is*, only what -/// happens on both sides of it. -pub const ANSWER_DEADLINE_NS: Nanos = 2_000_000_000; - -/// What the outstanding operation is for. The kernel's own enum, with the -/// fields it carries only to name things in a log dropped. -enum What { - /// The port's device has gone: its blocks are the next device's the moment - /// its slot is, and the port becomes one the machine may enumerate again. - SlotGone, - /// A device given up on while it is still in its port, which is why the - /// port stays marked attached and its blocks stay claimed. - LetGo, - Recovering(Recovery), - /// One act of an enumeration. `act` travels with the sequence because what - /// an answer *teaches* is a function of what was asked; `trained` does - /// because the record the port gets at the end is about the link the - /// enumeration started on, which by then is several passes ago. - Enumerating { seq: Enumeration, act: enumerate::Act, trained: bool }, -} - -/// How many steps one observation may produce before the machine is declared -/// stuck. Generous: the longest legitimate run is teardown, acknowledge, -/// debounce — four. -const STEP_BUDGET: usize = 16; - -pub struct Driver { - state: PortState, - /// What Enable Slot answers, so a scenario can stage a device the - /// controller has no slot for. - slot: Option, - /// The slot *this* enumeration actually got, which is `None` until Enable - /// Slot has answered — and stays `None` where it never did. The two are - /// different questions and conflating them would have a port report a slot - /// the controller was never asked to allocate, and a teardown then disable - /// it. - spent: Option, - /// Ask the machine what to do from *inside* an effect it is having - /// performed, which is what a driver that polled its own ports from within - /// an enumeration would do. The enumeration drains the event ring, so this - /// is reachable rather than hypothetical. - reenter: bool, - pull_as_it_enumerates: bool, - /// Enumerate and tear down without asking whether the controller still owes - /// an answer, which is the negative gate for the deferral. - never_defers: bool, - /// Keep an endpoint recovery or an enumeration running after its port has - /// gone, which is the negative gate for the cancellation. - never_cancels: bool, - /// What the device in the port says it is, which is the one branch an - /// enumeration's order depends on. - function: enumerate::Function, - /// Give the pool block back the instant Disable Slot is submitted, which is - /// the negative gate for the teardown's order. - frees_blocks_early: bool, - - outstanding: Outstanding, - /// Completions the controller owes: where the command was, what it will say - /// and when. A `Vec` because a command submitted while another is owed is - /// exactly what the deferral is supposed to make impossible. - owed: Vec<(u64, u32, Nanos)>, - next_trb: u64, - pub answers: Answers, - - /// Slots the controller took back, which is what a replug that leaks one - /// would leave short. - pub disabled: usize, - /// Whether the pool block this port's device claimed is still spoken for. - block_held: bool, - /// What the controller publishes for the bound device's interrupt - /// endpoint, and `None` where there is no bound device. - endpoint: Option, - /// The endpoint delivered a completion this driver did not expect. - broke: bool, - - pub did: Vec, - /// Every command and control request the recovery issued, by the name the - /// driver logs it under. - pub commands: Vec<&'static str>, - /// Recoveries abandoned because the port they were for has gone. - pub cancelled: usize, - /// Enumerations abandoned for the same reason. - pub abandoned: usize, - /// Every act an enumeration issued, in order. - pub acts: Vec, - /// Times the endpoint was handed its next transfer, which is what "the - /// device is delivering again" means. - pub requeues: usize, - /// Devices let go because their endpoint could not be restarted. - pub let_go: usize, - pub wake_at: Option, -} - -impl Driver { - pub fn new() -> Self { - Self { - state: PortState::EMPTY, - slot: Some(1), - spent: None, - reenter: false, - pull_as_it_enumerates: false, - never_defers: false, - never_cancels: false, - function: enumerate::Function::BootHid, - frees_blocks_early: false, - outstanding: Outstanding::EMPTY, - owed: Vec::new(), - next_trb: 0x1000, - answers: Answers::After(0), - disabled: 0, - block_held: false, - endpoint: None, - broke: false, - did: Vec::new(), - commands: Vec::new(), - cancelled: 0, - abandoned: 0, - acts: Vec::new(), - requeues: 0, - let_go: 0, - wake_at: None, - } - } - - pub fn with_flaw(flaw: Flaw) -> Self { - Self { state: PortState::with_flaw(flaw), ..Self::new() } - } - - /// A driver that read the controller's Supported Protocol capability and so - /// knows what this port speaks. - pub fn speaking(mut self, protocol: Protocol) -> Self { - self.state.speaks(Some(protocol)); - self - } - - /// Every reset the driver issued, in order — the assertion for a port that - /// should have needed none. - pub fn resets(&self) -> Vec { - self.did - .iter() - .filter_map(|d| match d { - Did::Reset(kind) => Some(*kind), - _ => None, - }) - .collect() - } - - /// Stage an enumeration that produces no slot. - pub fn without_slot(mut self) -> Self { - self.slot = None; - self - } - - /// Stage a caller that re-enters the machine from inside an effect. - pub fn reentrant(mut self) -> Self { - self.reenter = true; - self - } - - /// Stage a device pulled between the step that says enumerate and the - /// enumeration's first read of the port. - pub fn pulled_as_it_enumerates(mut self) -> Self { - self.pull_as_it_enumerates = true; - self - } - - /// Stage a device whose configuration descriptor names this function, which - /// is the one branch an enumeration's order depends on. - pub fn presenting(mut self, function: enumerate::Function) -> Self { - self.function = function; - self - } - - /// Stage a controller that answers this way. - pub fn answering(mut self, answers: Answers) -> Self { - self.answers = answers; - self - } - - /// Stage the driver that acts on a port without asking whether the - /// controller still owes it an answer. - pub fn never_defers(mut self) -> Self { - self.never_defers = true; - self - } - - /// Stage the driver that keeps a recovery running after its port has gone. - pub fn never_cancels(mut self) -> Self { - self.never_cancels = true; - self - } - - /// Stage the driver that gives a pool block back the instant Disable Slot - /// is submitted, while the endpoint contexts in it are still the - /// controller's to write. - pub fn frees_blocks_early(mut self) -> Self { - self.frees_blocks_early = true; - self - } - - /// Bind a device with an interrupt endpoint in this state, as - /// [`Did::Enumerated`] would have left it. - pub fn with_endpoint(mut self, state: EndpointState) -> Self { - self.endpoint = Some(state); - self - } - - pub fn attached(&self) -> bool { - self.state.attached() - } - - /// Whether the port machine has something in hand. Not the same question as - /// [`Self::busy`]: a port at rest can still be waiting on a slot. - pub fn outstanding(&self) -> bool { - self.state.outstanding() - } - - /// Whether the controller owes this driver an answer. - pub fn busy(&self) -> bool { - self.outstanding.busy() - } - - pub fn block_held(&self) -> bool { - self.block_held - } - - /// The endpoint's state as the controller publishes it. - pub fn endpoint(&self) -> Option { - self.endpoint - } - - /// The endpoint delivered a completion the driver did not expect, which is - /// what starts a recovery. - pub fn endpoint_broke(&mut self) { - self.broke = true; - } - - /// Everything the driver has to do at `now`, with every step checked - /// against the word that produced it. - pub fn pump(&mut self, port: &mut FakePort, now: Nanos) -> Result<(), Stuck> { - self.pass(port, now)?; - if self.state.outstanding() && self.wake_at.is_none() { - return Err(Stuck::Unwoken); - } - Ok(()) - } - - fn pass(&mut self, port: &mut FakePort, now: Nanos) -> Result<(), Stuck> { - port.tick(now); - self.collect(now); - self.advance(now)?; - self.recover(port, now); - if !port::due(port.take_event(), core::slice::from_ref(&self.state)) { - self.wake_at = self.outstanding.wake_at(); - return Ok(()); - } - for _ in 0..STEP_BUDGET { - let read = port.read(); - if !read.connected() || read.connect_changed() { - self.cancel_recovery(); - self.cancel_enumeration(); - } - // A port inside an effect a previous pass began is not decided - // about until the controller has answered for it. - if self.state.working().is_some() { - self.wake_at = self.outstanding.wake_at(); - return Ok(()); - } - let busy = if self.never_defers { None } else { self.outstanding.wake_at() }; - let before = self.state; - let step = self.state.step(read, now); - if let Some(bad) = invariants::check(&before, &step, read, now) { - return Err(Stuck::Broke(bad)); - } - match step { - Step::Idle => { - self.wake_at = self.outstanding.wake_at(); - return Ok(()); - } - Step::Wait(at) => { - self.wake_at = Some(at); - return Ok(()); - } - Step::GaveUp(why) => self.did.push(Did::GaveUp(why)), - Step::Write(write) => port.write(write.raw(), now), - Step::Reset(kind, write) => { - self.did.push(Did::Reset(kind)); - port.write(write.raw(), now); - } - Step::Teardown(why, pending) => { - if let Some(at) = busy { - self.wake_at = Some(at); - return Ok(()); - } - pending.running(); - // Between here and the report, the port is inside an - // effect. A step taken now is the re-entrancy the invariant - // names, and the simulator stages exactly that below. - if self.reenter { - let read = port.read(); - let before = self.state; - let step = self.state.step(read, now); - if let Some(bad) = invariants::check(&before, &step, read, now) { - return Err(Stuck::Broke(bad)); - } - } - self.did.push(Did::ToreDown(why)); - if self.tear_down(now)? { - self.state.torn_down(); - } else { - self.wake_at = self.outstanding.wake_at(); - return Ok(()); - } - } - Step::Enumerate { after, pending } => { - if let Some(at) = busy { - self.wake_at = Some(at); - return Ok(()); - } - pending.running(); - if self.reenter { - let read = port.read(); - let before = self.state; - let step = self.state.step(read, now); - if let Some(bad) = invariants::check(&before, &step, read, now) { - return Err(Stuck::Broke(bad)); - } - } - if core::mem::take(&mut self.pull_as_it_enumerates) { - port.detach(); - } - if self.outstanding.busy() { - return Err(Stuck::Order(Broke::ActedWithAnAnswerOutstanding)); - } - // `port::enumeration_ack`, the function `device::begin` - // writes: the flaw is the only thing composed here. - let fresh = port.read(); - let ack = if self.state.has_flaw(Flaw::WriteBackWhatWasRead) { - toyos_xhci::portsc::Write::whole_word(fresh) - } else { - port::enumeration_ack(after, fresh) - }; - if let Some(bad) = invariants::check_write(ack, fresh) { - return Err(Stuck::Broke(bad)); - } - port.write(ack.raw(), now); - // `device::begin`'s refusal of a port its acknowledge left - // disabled: no command is spent, and the port is read again. - if !port.read().enabled() { - self.enumerated(after.is_none()); - continue; - } - // Submitted and left, exactly as the teardown is: the port - // stays inside the effect until the last act is answered, - // and the check above catches a step taken meanwhile. - let (seq, act) = Enumeration::begin(); - self.issue_act(seq, act, after.is_none(), now); - self.wake_at = self.outstanding.wake_at(); - return Ok(()); - } - } - port.tick(now); - } - Err(Stuck::NoProgress) - } - - /// Everything a device that is no longer on the bus leaves behind, in the - /// order the pieces stop being reachable. `true` when nothing is owed. - fn tear_down(&mut self, now: Nanos) -> Result { - self.endpoint = None; - self.broke = false; - if self.outstanding.busy() { - return Err(Stuck::Order(Broke::ActedWithAnAnswerOutstanding)); - } - if self.state.take_slot().is_none() { - self.release_block()?; - return Ok(true); - } - self.submit(What::SlotGone, now); - if self.frees_blocks_early { - self.release_block()?; - } - Ok(false) - } - - fn release_block(&mut self) -> Result<(), Stuck> { - if self.outstanding.busy() { - return Err(Stuck::Order(Broke::ReleasedABlockWithItsSlotOutstanding)); - } - self.block_held = false; - Ok(()) - } - - /// Submit a command and record what its answer is owed to. - fn submit(&mut self, what: What, now: Nanos) { - let trb = self.next_trb; - self.next_trb += 16; - let answer = match self.answers { - Answers::After(delay) => Some((CC_SUCCESS, now + delay)), - Answers::Refuses(code) => Some((code, now)), - Answers::Never => None, - }; - if let Some((code, at)) = answer { - self.owed.push((trb, code, at)); - } - self.outstanding.submit(what, Await::Command { trb }, Stages::One, now + ANSWER_DEADLINE_NS); - } - - /// Hand the controller's answers to whatever is waiting for them. - fn collect(&mut self, now: Nanos) { - let mut still_owed = Vec::new(); - for (trb, code, at) in core::mem::take(&mut self.owed) { - if at > now { - still_owed.push((trb, code, at)); - continue; - } - self.outstanding.answered(Await::Command { trb }, code, 0); - } - self.owed = still_owed; - } - - /// Act on whatever the controller has answered. - fn advance(&mut self, now: Nanos) -> Result<(), Stuck> { - while let Some((what, outcome)) = self.outstanding.finished(now) { - match what { - What::SlotGone => { - if outcome.succeeded() { - self.disabled += 1; - } - // The block goes back whatever the controller said: a port - // whose device has left holding one for the life of the - // boot is the worse failure, and a controller that will not - // disable a slot is already past what a driver repairs. - self.release_block()?; - self.state.torn_down(); - } - What::LetGo => { - if outcome.succeeded() { - self.disabled += 1; - } - } - What::Recovering(mut seq) => { - if !outcome.succeeded() { - self.give_up(now); - continue; - } - let act = seq.completed(); - self.issue(seq, act, now); - } - What::Enumerating { seq, act, trained } => { - // A device that stopped answering ends the enumeration - // where it is, and the port keeps whatever slot was spent - // on it — only Disable Slot gives one back. - if !outcome.succeeded() { - self.enumerated(trained); - continue; - } - if act == enumerate::Act::Command(enumerate::Command::EnableSlot) { - self.spent = self.slot; - } - match seq.completed(self.learnt(act)) { - Next::Act(seq, act) => self.issue_act(seq, act, trained, now), - Next::Bind | Next::Refuse => self.enumerated(trained), - } - } - } - } - Ok(()) - } - - /// What the device in the port teaches the sequence about the order of what - /// is left. Only the configuration descriptor carries any. - fn learnt(&self, act: enumerate::Act) -> Learnt { - match act { - enumerate::Act::Request(Request::ConfigDescriptor) => Learnt::Function(self.function), - _ => Learnt::Nothing, - } - } - - fn issue_act(&mut self, seq: Enumeration, act: enumerate::Act, trained: bool, now: Nanos) { - self.acts.push(act); - self.submit(What::Enumerating { seq, act, trained }, now); - } - - /// The enumeration is over, however it went. The port records the slot, - /// which is what a later teardown gives back. - fn enumerated(&mut self, trained: bool) { - let slot = self.spent.take(); - self.did.push(Did::Enumerated { slot, trained }); - self.block_held = slot.is_some(); - self.state.enumerated(slot.and_then(NonZeroU8::new)); - } - - /// Start the bound endpoint's recovery, if one is owed and the controller - /// is not already answering something else. - fn recover(&mut self, port: &FakePort, now: Nanos) { - if !self.broke || self.outstanding.busy() { - return; - } - self.broke = false; - let read = port.read(); - // **When the disconnect and the transfer error race, the disconnect - // wins.** CSC as well as CCS: a device replugged between two looks - // reads connected again, and the transfer that died still died with the - // old one. - if !read.connected() || read.connect_changed() { - self.commands.push("left to the disconnect"); - return; - } - let Some(state) = self.endpoint else { return }; - match Recovery::begin(state) { - Ok((seq, act)) => self.issue(seq, act, now), - Err(NeedsConfigure(_)) => self.give_up(now), - } - } - - /// A device whose endpoint could not be restarted, off the bus — but with - /// its port still marked attached, because a port whose belief goes empty - /// with the device still in it reads as a fresh connect and gets the same - /// endpoint enumerated again every debounce. - fn give_up(&mut self, now: Nanos) { - self.let_go += 1; - self.endpoint = None; - if self.state.take_slot().is_some() { - self.submit(What::LetGo, now); - } - } - - fn issue(&mut self, seq: Recovery, act: Act, now: Nanos) { - match act { - Act::Command(cmd) => { - self.commands.push(cmd.name()); - self.submit(What::Recovering(seq), now); - } - Act::ClearHalt => { - self.commands.push("CLEAR_FEATURE(ENDPOINT_HALT)"); - self.submit(What::Recovering(seq), now); - } - Act::Running => { - self.requeues += 1; - self.endpoint = Some(EndpointState::Running); - } - } - } - - /// Drop a recovery outstanding for a device whose port has gone. The - /// command it waits for will not be answered by anything still on the bus, - /// and the teardown behind it would spend the whole deadline finding out. - fn cancel_recovery(&mut self) { - if self.never_cancels { - return; - } - if !matches!(self.outstanding.what(), Some(What::Recovering(_))) { - return; - } - self.outstanding.cancel(); - self.cancelled += 1; - } - - /// The same for an enumeration, and for the same reason — **except while - /// Enable Slot is the act outstanding.** Its answer *is* the slot id, so a - /// driver that stopped listening for it would leak a Device Slot the - /// controller has already allocated. - fn cancel_enumeration(&mut self) { - if self.never_cancels { - return; - } - let Some(What::Enumerating { act, trained, .. }) = self.outstanding.what() else { - return; - }; - if *act == enumerate::Act::Command(enumerate::Command::EnableSlot) { - return; - } - let trained = *trained; - self.outstanding.cancel(); - self.abandoned += 1; - self.enumerated(trained); - } - - /// Run the port forward to `deadline`, waking whenever the machine asked to - /// be woken. `step` is how finely the clock is sampled between wakes, which - /// is what a scheduler pass on a busy machine looks like. - pub fn run_to( - &mut self, - port: &mut FakePort, - from: Nanos, - deadline: Nanos, - step: Nanos, - ) -> Result { - let mut now = from; - while now < deadline { - self.pump(port, now)?; - now = match self.wake_at { - Some(at) if at <= now => return Err(Stuck::NoProgress), - Some(at) => at.min(now + step), - None => now + step, - }; - } - self.pump(port, deadline)?; - Ok(deadline) - } - - pub fn enumerations(&self) -> usize { - self.did.iter().filter(|d| matches!(d, Did::Enumerated { .. })).count() - } - - pub fn teardowns(&self) -> usize { - self.did.iter().filter(|d| matches!(d, Did::ToreDown(_))).count() - } -} - -impl Default for Driver { - fn default() -> Self { - Self::new() - } -} diff --git a/lib/xhci/sim/src/lib.rs b/lib/xhci/sim/src/lib.rs deleted file mode 100644 index b478c20854f..00000000000 --- a/lib/xhci/sim/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! The host simulator for the xHCI port machine. -//! -//! A fake port register with hardware's own write rules, the loop the kernel -//! runs, and the scenarios and negative gates that decide whether the machine -//! is right. - -pub mod driver; -pub mod hub; diff --git a/lib/xhci/sim/tests/enumerate.rs b/lib/xhci/sim/tests/enumerate.rs deleted file mode 100644 index c64310ef643..00000000000 --- a/lib/xhci/sim/tests/enumerate.rs +++ /dev/null @@ -1,224 +0,0 @@ -//! What an enumeration must do once it is not allowed to wait. -//! -//! **These are the states QEMU cannot stage.** A controller that answers Enable -//! Slot and then stops is not a device or a machine property; a device pulled -//! between two acts of its own enumeration needs the unplug landed inside a -//! window a few microseconds wide on real hardware, and `device_del` cannot be -//! aimed there. Every timing here is a constant of the machine or of the -//! simulator, never a literal. - -use toyos_xhci::enumerate::{Act, Command, Function, Request}; -use toyos_xhci::port::{Nanos, DEBOUNCE_NS}; -use toyos_xhci_sim::driver::{Answers, Did, Driver, ANSWER_DEADLINE_NS}; -use toyos_xhci_sim::hub::{FakePort, ResetBehaviour}; - -/// A reset that finishes quickly, which is every healthy port. -const QUICK: ResetBehaviour = ResetBehaviour::Completes { after: 1_000_000 }; -/// How finely the simulated scheduler samples the clock between wakes. -const PASS: Nanos = 1_000_000; -/// Long enough for a device to be bound and settled. -const SETTLED: Nanos = 4 * DEBOUNCE_NS; -/// What an unplug costs when the enumeration in flight is abandoned: a -/// debounce, then the teardown's own Disable Slot against a controller that has -/// stopped answering. Leaving the enumeration running adds a second one, and -/// that gap is what the gate below measures. -const ONE_DEADLINE: Nanos = DEBOUNCE_NS + ANSWER_DEADLINE_NS + DEBOUNCE_NS; - -/// Step until the driver has an act outstanding, and say when that was. The -/// enumeration starts a debounce after the connect, so the caller cannot name -/// the instant itself without restating the machine's own constant. -fn until_busy(driver: &mut Driver, port: &mut FakePort, until: Nanos) -> Nanos { - let mut now = 0; - while now < until { - driver.pump(port, now).unwrap(); - if driver.busy() { - return now; - } - now += PASS; - } - panic!("the driver never submitted anything: {:?}", driver.did); -} - -/// Run until the port has been torn down and the controller owes nothing, and -/// say when that was. -/// -/// Not "no longer attached": a port mid-enumeration has not been reported yet, -/// so it reads unattached from the first instant and every measurement against -/// it would be zero. -fn free_at(driver: &mut Driver, port: &mut FakePort, from: Nanos, until: Nanos) -> Option { - let mut now = from; - while now < until { - driver.pump(port, now).unwrap(); - if driver.teardowns() > 0 && !driver.busy() && !driver.attached() { - return Some(now); - } - now += PASS; - } - None -} - -/// The acts a boot-protocol HID device's enumeration issues, which is the order -/// [`toyos_xhci::enumerate`] decides and this driver performs. -const BOOT_HID: [Act; 8] = [ - Act::Command(Command::EnableSlot), - Act::Command(Command::AddressDevice), - Act::Request(Request::DeviceDescriptor { want: 8 }), - Act::Request(Request::DeviceDescriptor { want: 18 }), - Act::Request(Request::ConfigDescriptor), - Act::Request(Request::SetConfiguration), - Act::Request(Request::SetProtocol), - Act::Command(Command::ConfigureEndpoint), -]; - -/// The property the whole conversion is for: no single pass carries an -/// enumeration from a connected port to a bound device. A driver that waited -/// could not be written in this file at all, so what is checked here is that -/// this one does not — and that the port is reported once at the end rather -/// than once per act. -#[test] -fn an_enumeration_takes_one_act_per_pass_and_reports_once() { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::new(); - - let began = until_busy(&mut driver, &mut port, SETTLED); - assert_eq!(driver.acts, [Act::Command(Command::EnableSlot)]); - assert_eq!(driver.enumerations(), 0, "a pass bound the device: {:?}", driver.did); - - driver.run_to(&mut port, began, SETTLED, PASS).unwrap(); - assert_eq!(driver.acts, BOOT_HID); - assert_eq!(driver.did.last(), Some(&Did::Enumerated { slot: Some(1), trained: false })); - assert!(driver.block_held()); - assert!(!driver.busy(), "the controller is still owed something"); -} - -/// A disk and a tablet have no boot protocol to select. The order is -/// [`toyos_xhci::enumerate`]'s to decide and this only checks the driver asks -/// for what it is told to. -#[test] -fn a_device_with_no_boot_protocol_is_never_asked_for_one() { - for function in [Function::Msc, Function::Hid] { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::new().presenting(function); - driver.run_to(&mut port, 0, SETTLED, PASS).unwrap(); - assert_eq!(driver.enumerations(), 1, "{function:?}: {:?}", driver.did); - assert!( - !driver.acts.contains(&Act::Request(Request::SetProtocol)), - "{function:?} was asked for a boot protocol it has none of: {:?}", - driver.acts - ); - } -} - -/// A controller that answers Enable Slot and then stops. The port must still be -/// reported — with the slot, because only Disable Slot gives one back — rather -/// than staying inside an effect nothing will ever finish. -#[test] -fn an_enumeration_that_stops_being_answered_still_reports_its_slot() { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::new(); - let began = until_busy(&mut driver, &mut port, SETTLED); - // Past Enable Slot, so the controller has named a slot to be reported. - driver.pump(&mut port, began + PASS).unwrap(); - assert_eq!(driver.acts.len(), 2, "{:?}", driver.acts); - - driver.answers = Answers::Never; - let end = began + 2 * ANSWER_DEADLINE_NS; - driver.run_to(&mut port, began, end, PASS).unwrap(); - - assert_eq!(driver.did.last(), Some(&Did::Enumerated { slot: Some(1), trained: false })); - assert!(!driver.busy()); - // And the slot is still reachable, so the unplug behind it gives it back. - driver.answers = Answers::After(0); - port.detach(); - driver.run_to(&mut port, end, end + 4 * DEBOUNCE_NS, PASS).unwrap(); - assert_eq!(driver.disabled, 1, "the slot the enumeration spent was never given back"); -} - -/// **A transfer error on a port that has gone belongs to the disconnect.** The -/// device will not answer the act outstanding against it, so the teardown -/// behind it would spend a whole deadline finding that out. -#[test] -fn an_enumeration_is_abandoned_when_its_port_goes() { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::new(); - // Past Enable Slot, which is the one act that is never abandoned. - let began = until_busy(&mut driver, &mut port, SETTLED); - driver.pump(&mut port, began + PASS).unwrap(); - assert_eq!(driver.acts.len(), 2, "{:?}", driver.acts); - - driver.answers = Answers::Never; - port.detach(); - let end = began + 3 * ANSWER_DEADLINE_NS; - let at = free_at(&mut driver, &mut port, began, end).expect("the port was never freed"); - - assert_eq!(driver.abandoned, 1, "the enumeration was waited out instead"); - assert!( - at < began + ONE_DEADLINE, - "the port came back at {} ms, which is the enumeration's deadline as well as the \ - teardown's", - (at - began) / 1_000_000 - ); -} - -/// **The negative gate for that cancellation.** With it off, the teardown waits -/// out the deadline of an act whose device left the bus — which is the second -/// of the two costs an unplug used to pay, and the one no `device_del` can be -/// aimed at. -#[test] -fn gate_an_enumeration_that_outlives_its_port_costs_a_deadline() { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::new().never_cancels(); - let began = until_busy(&mut driver, &mut port, SETTLED); - driver.pump(&mut port, began + PASS).unwrap(); - - driver.answers = Answers::Never; - port.detach(); - let end = began + 4 * ANSWER_DEADLINE_NS; - let at = free_at(&mut driver, &mut port, began, end).expect("the port was never freed"); - - assert_eq!(driver.abandoned, 0, "the flaw did not change the outcome"); - assert!( - at > began + ONE_DEADLINE, - "the port came back at {} ms, so the flaw cost nothing and this gate proves nothing", - (at - began) / 1_000_000 - ); -} - -/// **Enable Slot is the one act that is never abandoned**, because its answer -/// *is* the slot id: a driver that stopped listening for it would leak a Device -/// Slot the controller has already allocated, and nothing would ever name it -/// again. -#[test] -fn a_slot_the_controller_has_not_named_yet_is_waited_for_even_when_the_port_goes() { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::new().answering(Answers::Never); - let began = until_busy(&mut driver, &mut port, SETTLED); - assert_eq!(driver.acts, [Act::Command(Command::EnableSlot)]); - - port.detach(); - driver.run_to(&mut port, began, began + ANSWER_DEADLINE_NS / 2, PASS).unwrap(); - - assert_eq!(driver.abandoned, 0, "the Enable Slot was abandoned and its slot with it"); - assert!(driver.busy(), "nothing is listening for the slot the controller was asked for"); -} - -/// The same Enable Slot, waited out. Its unplug's edge was spent while the port -/// was inside the enumeration, so the report that ends it is what has the port -/// read again, and the device that left is taken down a debounce later. -#[test] -fn a_device_pulled_while_enable_slot_goes_unanswered_is_torn_down() { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::new().answering(Answers::Never); - let began = until_busy(&mut driver, &mut port, SETTLED); - port.detach(); - let end = began + ANSWER_DEADLINE_NS + 2 * DEBOUNCE_NS; - driver.run_to(&mut port, began, end, PASS).unwrap(); - - assert!( - driver.did.contains(&Did::Enumerated { slot: None, trained: false }), - "the Enable Slot's deadline never ended the enumeration: {:?}", - driver.did - ); - assert_eq!(driver.teardowns(), 1, "the device that left was never taken down: {:?}", driver.did); - assert!(!driver.attached(), "{:?}", driver.did); -} diff --git a/lib/xhci/sim/tests/explore.rs b/lib/xhci/sim/tests/explore.rs deleted file mode 100644 index 94b8323566f..00000000000 --- a/lib/xhci/sim/tests/explore.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! Sequences nobody chose. -//! -//! The staged scenarios cover what a person does to a port. This covers what a -//! marginal cable does: attach, detach and replug at instants no test author -//! would pick, with the invariants checked after every step and two properties -//! checked after every sequence. -//! -//! Seeded and reported, so a failure is a number that reproduces it. - -use rand::{Rng, SeedableRng}; - -use toyos_xhci::port::{DEBOUNCE_NS, RESET_DEADLINE_NS}; -use toyos_xhci_sim::driver::{Did, Driver}; -use toyos_xhci_sim::hub::{FakePort, ResetBehaviour}; - -const PASS: u64 = 1_000_000; - -/// How many sequences one run explores, and how long each is. Small enough to -/// stay inside a `cargo test` nobody is waiting for. -const SEQUENCES: u64 = 2_000; -const EVENTS: usize = 24; - -/// One sequence, replayable from its seed. -fn explore(seed: u64) -> Result<(), String> { - let mut rng = rand::rngs::SmallRng::seed_from_u64(seed); - let behaviour = if rng.random_bool(0.15) { - ResetBehaviour::Never - } else { - ResetBehaviour::Completes { after: rng.random_range(1..=(4 * PASS)) } - }; - let mut port = FakePort::empty(behaviour); - let mut driver = Driver::new(); - let mut now: u64 = 0; - - for _ in 0..EVENTS { - match rng.random_range(0..4) { - 0 => port.attach(), - 1 => port.detach(), - 2 => port.replug(), - _ => {} - } - // Anything from a scheduler pass to well past the reset deadline, so - // both "the driver looks again immediately" and "nothing looks for - // seconds" are covered. - let gap = rng.random_range(1..=(DEBOUNCE_NS + RESET_DEADLINE_NS / 2)); - let until = now + gap; - driver - .run_to(&mut port, now, until, PASS) - .map_err(|e| format!("seed {seed}: {e:?} after {:?}", driver.did))?; - now = until; - } - - // **Balance.** The driver takes a port on and gives it back alternately, - // never twice in a row either way — so it never holds two slots for one - // port and never leaves one bound to a device that has gone. - // - // Giving up counts as taking it on. That is not a technicality: the refusal - // deliberately marks the port attached so it is not reset again every pass, - // and the teardown when the device is finally pulled is what lets the next - // one be tried. A property that paired teardowns with *enumerations* alone - // called that legitimate sequence a leak. - let mut held = 0i32; - for did in &driver.did { - held += match did { - Did::Enumerated { .. } | Did::GaveUp(_) => 1, - // A reset is a step on the way in, not a claim on the port. - Did::Reset(_) => 0, - Did::ToreDown(_) => -1, - }; - if !(0..=1).contains(&held) { - return Err(format!("seed {seed}: port held {held} times over in {:?}", driver.did)); - } - } - - // **Agreement.** What the driver believes about the port is what its own - // record of what it did says, which is the property a lost teardown breaks. - if driver.attached() != (held == 1) { - return Err(format!( - "seed {seed}: believes attached={} after {:?}", - driver.attached(), - driver.did - )); - } - Ok(()) -} - -#[test] -fn no_sequence_breaks_an_invariant() { - let mut failures = Vec::new(); - for seed in 0..SEQUENCES { - if let Err(why) = explore(seed) { - failures.push(why); - if failures.len() >= 3 { - break; - } - } - } - assert!(failures.is_empty(), "{}", failures.join("\n")); - eprintln!(" [xhci-sim] {SEQUENCES} sequences of {EVENTS} events, no violation"); -} diff --git a/lib/xhci/sim/tests/scenarios.rs b/lib/xhci/sim/tests/scenarios.rs deleted file mode 100644 index e350b16ac31..00000000000 --- a/lib/xhci/sim/tests/scenarios.rs +++ /dev/null @@ -1,250 +0,0 @@ -//! What the port machine must do, staged as sequences a person can perform. -//! -//! Every timing here is the machine's own constant, never a literal: a test -//! that hard-codes 100 ms is a test that stops meaning anything the day the -//! debounce moves. - -use toyos_xhci::port::{Flaw, GaveUp, Gone, Reset, DEBOUNCE_NS, RESET_DEADLINE_NS}; -use toyos_xhci_sim::driver::{Did, Driver, Stuck}; -use toyos_xhci_sim::hub::{FakePort, ResetBehaviour}; - -/// A reset that finishes quickly, which is every healthy port. -const QUICK: ResetBehaviour = ResetBehaviour::Completes { after: 1_000_000 }; -/// How finely the simulated scheduler samples the clock between wakes. -const PASS: u64 = 1_000_000; - -#[test] -fn a_device_plugged_in_is_enumerated_once() { - let mut port = FakePort::empty(QUICK); - let mut driver = Driver::new(); - driver.pump(&mut port, 0).unwrap(); - assert_eq!(driver.did, [], "an empty port is not work"); - - port.attach(); - driver.run_to(&mut port, 0, 4 * DEBOUNCE_NS, PASS).unwrap(); - assert_eq!(driver.enumerations(), 1, "{:?}", driver.did); - assert!(driver.attached()); - assert!(!driver.outstanding(), "the port should be at rest afterwards"); -} - -#[test] -fn the_debounce_is_actually_waited_out() { - let mut port = FakePort::empty(QUICK); - let mut driver = Driver::new(); - port.attach(); - // One nanosecond short of the interval USB 2.0 §7.1.7.3 requires. - driver.run_to(&mut port, 0, DEBOUNCE_NS - 1, PASS).unwrap(); - assert_eq!(driver.enumerations(), 0, "enumerated inside the debounce: {:?}", driver.did); - driver.run_to(&mut port, DEBOUNCE_NS - 1, 4 * DEBOUNCE_NS, PASS).unwrap(); - assert_eq!(driver.enumerations(), 1); -} - -#[test] -fn a_device_that_bounces_back_out_is_never_enumerated() { - let mut port = FakePort::empty(QUICK); - let mut driver = Driver::new(); - port.attach(); - driver.run_to(&mut port, 0, DEBOUNCE_NS / 2, PASS).unwrap(); - port.detach(); - driver.run_to(&mut port, DEBOUNCE_NS / 2, 8 * DEBOUNCE_NS, PASS).unwrap(); - assert_eq!(driver.did, [], "{:?}", driver.did); - assert!(!driver.attached()); -} - -#[test] -fn unplugging_tears_the_device_down() { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::new(); - driver.run_to(&mut port, 0, 4 * DEBOUNCE_NS, PASS).unwrap(); - assert_eq!(driver.enumerations(), 1); - - port.detach(); - driver.run_to(&mut port, 4 * DEBOUNCE_NS, 12 * DEBOUNCE_NS, PASS).unwrap(); - assert_eq!(driver.teardowns(), 1, "{:?}", driver.did); - assert!(matches!(driver.did.last(), Some(Did::ToreDown(Gone::Disconnected)))); - assert!(!driver.attached()); -} - -/// **The case the level cannot report.** The device is pulled and pushed back -/// inside one debounce, so CCS reads the same at both ends and only CSC says -/// anything happened. The old device must come down before the new one is -/// enumerated, or the slot stays bound to something that has gone. -#[test] -fn a_replug_inside_one_debounce_is_seen() { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::new(); - driver.run_to(&mut port, 0, 4 * DEBOUNCE_NS, PASS).unwrap(); - assert_eq!(driver.enumerations(), 1); - let settled = 4 * DEBOUNCE_NS; - - port.replug(); - driver.run_to(&mut port, settled, settled + 8 * DEBOUNCE_NS, PASS).unwrap(); - - assert_eq!(driver.teardowns(), 1, "the device that left was not taken down: {:?}", driver.did); - assert!(driver.did.contains(&Did::ToreDown(Gone::Replugged))); - assert_eq!(driver.enumerations(), 2, "what is in the port now was not brought up"); - assert!(driver.attached()); -} - -#[test] -fn a_port_that_never_finishes_its_reset_is_given_up_on_and_left_alone() { - let mut port = FakePort::empty(ResetBehaviour::Never); - let mut driver = Driver::new(); - port.attach(); - driver - .run_to(&mut port, 0, DEBOUNCE_NS + RESET_DEADLINE_NS + PASS, PASS) - .unwrap(); - assert_eq!(driver.did, [Did::Reset(Reset::Hot), Did::GaveUp(GaveUp::ResetNeverFinished)], "{:?}", driver.did); - - // And it stays given up on: a port retried every pass is a port that costs - // the machine a reset per pass for as long as the device stays in it. - let before = driver.did.len(); - driver - .run_to( - &mut port, - DEBOUNCE_NS + RESET_DEADLINE_NS + PASS, - DEBOUNCE_NS + 4 * RESET_DEADLINE_NS, - PASS, - ) - .unwrap(); - assert_eq!(driver.did.len(), before, "the port was tried again: {:?}", driver.did); -} - -/// A hot reset raises no connect edge of its own, so the connect flag a hot -/// give-up leaves is a real replug, and what is in the port now is enumerated. -#[test] -fn a_device_replugged_inside_a_hot_reset_given_up_on_is_enumerated() { - let mut port = FakePort::occupied(ResetBehaviour::Completes { after: 50_000_000 }); - let mut driver = Driver::new(); - driver.run_to(&mut port, 0, DEBOUNCE_NS + 2 * PASS, PASS).unwrap(); - assert_eq!(driver.did, [Did::Reset(Reset::Hot)]); - // The detach ends the reset; the attach finds CSC already set. - port.replug(); - driver - .run_to(&mut port, DEBOUNCE_NS + 2 * PASS, 2 * RESET_DEADLINE_NS + 4 * DEBOUNCE_NS, PASS) - .unwrap(); - assert_eq!(driver.enumerations(), 1, "{:?}", driver.did); -} - -/// Four replugs, which is a person fidgeting with a cable. Each must produce -/// exactly one teardown and one enumeration, and the port must end bound. -#[test] -fn repeated_replugs_stay_balanced() { - const CYCLES: usize = 4; - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::new(); - let mut now = 0; - driver.run_to(&mut port, now, 4 * DEBOUNCE_NS, PASS).unwrap(); - now = 4 * DEBOUNCE_NS; - - for _ in 0..CYCLES { - port.replug(); - let until = now + 6 * DEBOUNCE_NS; - driver.run_to(&mut port, now, until, PASS).unwrap(); - now = until; - } - assert_eq!(driver.teardowns(), CYCLES, "{:?}", driver.did); - assert_eq!(driver.enumerations(), CYCLES + 1, "{:?}", driver.did); - assert!(driver.attached()); -} - -/// A device the controller has no slot for still leaves the port settled, and -/// the driver does not spin trying again. -#[test] -fn a_device_with_no_slot_settles_anyway() { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::new().without_slot(); - driver.run_to(&mut port, 0, 8 * DEBOUNCE_NS, PASS).unwrap(); - assert_eq!(driver.enumerations(), 1, "{:?}", driver.did); - assert!(!driver.outstanding()); -} - -// --------------------------------------------------------------------------- -// The negative gates. Each turns on one deliberate defect and requires the -// suite above to notice. A gate that cannot fail proves nothing, and these are -// what make the claim "the model covers the states QEMU cannot stage" mean -// something. -// --------------------------------------------------------------------------- - -/// Run the replug scenario against a flawed machine and say what happened. -fn replug_under(flaw: Flaw) -> Result<(usize, usize), Stuck> { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::with_flaw(flaw); - driver.run_to(&mut port, 0, 4 * DEBOUNCE_NS, PASS)?; - let settled = 4 * DEBOUNCE_NS; - port.replug(); - driver.run_to(&mut port, settled, settled + 8 * DEBOUNCE_NS, PASS)?; - Ok((driver.teardowns(), driver.enumerations())) -} - -/// Comparing CCS against what the driver believes, with CSC ignored, makes a -/// replug inside one debounce invisible: the port reads connected at both ends -/// and the slot stays bound to a device that has gone. -#[test] -fn gate_ignoring_the_connect_change_hides_a_replug() { - let (teardowns, enumerations) = replug_under(Flaw::IgnoreConnectChange).unwrap(); - assert_eq!((teardowns, enumerations), (0, 1), "the flaw did not change the outcome"); -} - -/// Clearing the change flags before deciding what they meant destroys the same -/// evidence one step earlier. -#[test] -fn gate_acknowledging_before_deciding_hides_a_replug() { - let (teardowns, enumerations) = replug_under(Flaw::AcknowledgeBeforeDeciding).unwrap(); - assert_eq!((teardowns, enumerations), (0, 1), "the flaw did not change the outcome"); -} - -/// Writing back the whole word carries PED, and PED is write-1-to-clear: the -/// driver disables the port it has just enabled. The invariant catches it -/// before the port does, which is the point of having one. -#[test] -fn gate_writing_back_what_was_read_disables_the_port() { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::with_flaw(Flaw::WriteBackWhatWasRead); - let outcome = driver.run_to(&mut port, 0, 8 * DEBOUNCE_NS, PASS); - assert!(outcome.is_err(), "a write that carries PED was not caught: {:?}", driver.did); -} - -/// Restarting the debounce on every observation means it never elapses, so a -/// device sitting perfectly still is never enumerated. -#[test] -fn gate_restarting_the_debounce_never_enumerates() { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::with_flaw(Flaw::RestartDebounce); - driver.run_to(&mut port, 0, 100 * DEBOUNCE_NS, PASS).unwrap(); - assert_eq!(driver.enumerations(), 0, "the flaw did not change the outcome"); -} - -/// A caller that asks the machine what to do from *inside* an effect it is -/// having performed. Reachable rather than hypothetical: an enumeration drains -/// the event ring, and what the driver does with a port event is step this -/// machine. Nothing here is a flaw — this is a sound machine and a re-entrant -/// caller, which is exactly what the old copy-out/write-back shape allowed -/// silently. -#[test] -fn gate_stepping_from_inside_an_effect_is_caught() { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::new().reentrant(); - let outcome = driver.run_to(&mut port, 0, 8 * DEBOUNCE_NS, PASS); - assert!( - matches!(outcome, Err(Stuck::Broke(_))), - "a re-entrant step was not caught: {outcome:?} after {:?}", - driver.did - ); -} - -/// Without the deadline a port that never resets is waited on forever, and the -/// machine makes no progress rather than refusing the port by name. -#[test] -fn gate_dropping_the_reset_deadline_wedges_the_port() { - let mut port = FakePort::empty(ResetBehaviour::Never); - let mut driver = Driver::with_flaw(Flaw::NoResetDeadline); - port.attach(); - let outcome = driver.run_to(&mut port, 0, 8 * RESET_DEADLINE_NS, PASS); - assert!(outcome.is_err(), "the flaw did not change the outcome: {:?}", driver.did); - assert!( - !driver.did.iter().any(|d| matches!(d, Did::GaveUp(_))), - "a flawed machine still refused the port by name: {:?}", - driver.did - ); -} diff --git a/lib/xhci/sim/tests/superspeed.rs b/lib/xhci/sim/tests/superspeed.rs deleted file mode 100644 index 4a0ba478cf6..00000000000 --- a/lib/xhci/sim/tests/superspeed.rs +++ /dev/null @@ -1,309 +0,0 @@ -//! What a SuperSpeed port needs, which is not what a USB2 port needs. -//! -//! **None of this is reachable in QEMU.** Its xHC has no link training and no -//! Inactive state; its SS ports read Enabled the moment the register is -//! touched, and no device or machine property makes a reset fail. So the guest -//! suite can only certify that nothing regressed, and correctness for these -//! paths lives here — which is why the negative gates beside them matter more -//! than usual. - -use toyos_xhci::port::{GaveUp, Gone, Reset, DEBOUNCE_NS, RESET_DEADLINE_NS}; -use toyos_xhci::Protocol; -use toyos_xhci_sim::driver::{Did, Driver}; -use toyos_xhci_sim::hub::{FakePort, ResetBehaviour}; - -const QUICK: ResetBehaviour = ResetBehaviour::Completes { after: 1_000_000 }; -const PASS: u64 = 1_000_000; - -/// **The fix, stated as the thing that must not happen.** A SuperSpeed link -/// trains itself and the port reads Enabled when it is up; there is nothing to -/// reset, and resetting anyway is a hot reset of a working link. -#[test] -fn a_trained_superspeed_link_is_not_reset() { - let mut port = FakePort::superspeed(QUICK); - let mut driver = Driver::new().speaking(Protocol::Usb3); - port.attach(); - driver.run_to(&mut port, 0, 4 * DEBOUNCE_NS, PASS).unwrap(); - - assert_eq!(driver.resets(), [], "a trained link was reset: {:?}", driver.did); - assert_eq!(driver.enumerations(), 1, "{:?}", driver.did); - assert!( - driver.did.contains(&Did::Enumerated { slot: Some(1), trained: true }), - "the port was brought up but not reported as already trained: {:?}", - driver.did - ); -} - -/// The same port on a driver that never read the protocol capability: it does -/// not know, so it does what it always did. This is what makes the test above -/// a statement about the *capability* rather than about SuperSpeed in general. -#[test] -fn a_port_of_unknown_protocol_is_still_reset() { - let mut port = FakePort::superspeed(QUICK); - let mut driver = Driver::new(); - port.attach(); - driver.run_to(&mut port, 0, 4 * DEBOUNCE_NS, PASS).unwrap(); - assert_eq!(driver.resets(), [Reset::Hot], "{:?}", driver.did); - assert_eq!(driver.enumerations(), 1); -} - -/// A USB2 port still needs its reset — the reset is how a device gets enabled -/// there at all, so the fix must not take it away from the ports that need it. -#[test] -fn a_usb2_port_is_reset_as_before() { - let mut port = FakePort::empty(QUICK); - let mut driver = Driver::new().speaking(Protocol::Usb2); - port.attach(); - driver.run_to(&mut port, 0, 4 * DEBOUNCE_NS, PASS).unwrap(); - assert_eq!(driver.resets(), [Reset::Hot]); - assert_eq!(driver.enumerations(), 1, "{:?}", driver.did); - assert!(driver.did.contains(&Did::Enumerated { slot: Some(1), trained: false })); -} - -/// **The laptop's USB-A ports.** A SuperSpeed port that is *not* enabled when the -/// device appears needs a reset — and the hot one takes the link down into -/// Inactive, which §4.19.1.2.4 says only a warm reset leaves. A driver with no -/// warm reset spends the deadline and refuses the port, which is a USB-A socket -/// that never mounts anything, twice out of two boots. -#[test] -fn a_link_a_hot_reset_kills_is_recovered_warm() { - let mut port = FakePort::empty(ResetBehaviour::HotResetKillsTheLink { warm_works: true }); - let mut driver = Driver::new().speaking(Protocol::Usb3); - port.attach(); - driver - .run_to(&mut port, 0, DEBOUNCE_NS + 2 * RESET_DEADLINE_NS, PASS) - .unwrap(); - - assert_eq!( - driver.resets(), - [Reset::Hot, Reset::Warm], - "the hot reset was not followed by a warm one: {:?}", - driver.did - ); - assert_eq!(driver.enumerations(), 1, "the port never came up: {:?}", driver.did); - assert!(driver.attached()); -} - -/// Once the link is Inactive the driver goes straight to the warm reset rather -/// than spending the deadline proving a hot one does not work. Same port, but -/// the link is already down when the driver first looks. -#[test] -fn a_link_already_inactive_is_warm_reset_at_once() { - let mut port = FakePort::empty(ResetBehaviour::HotResetKillsTheLink { warm_works: true }); - let mut driver = Driver::new().speaking(Protocol::Usb3); - port.attach(); - // Let the first hot reset take the link down, then start over with a fresh - // driver that finds the port in the state the last one left it. - driver.run_to(&mut port, 0, DEBOUNCE_NS + PASS, PASS).unwrap(); - let mut fresh = Driver::new().speaking(Protocol::Usb3); - port.replug(); - fresh - .run_to(&mut port, DEBOUNCE_NS + PASS, 4 * DEBOUNCE_NS + RESET_DEADLINE_NS, PASS) - .unwrap(); - assert_eq!( - fresh.resets(), - [Reset::Warm], - "an Inactive link was given a hot reset first: {:?}", - fresh.did - ); -} - -/// And a link that will not come back even warm is refused by a name that says -/// so, rather than by the name a USB2 port's failure would get. -#[test] -fn a_link_that_never_trains_is_refused_by_its_own_name() { - let mut port = FakePort::empty(ResetBehaviour::HotResetKillsTheLink { warm_works: false }); - let mut driver = Driver::new().speaking(Protocol::Usb3); - port.attach(); - driver - .run_to(&mut port, 0, DEBOUNCE_NS + 3 * RESET_DEADLINE_NS, PASS) - .unwrap(); - assert_eq!(driver.resets(), [Reset::Hot, Reset::Warm], "{:?}", driver.did); - assert_eq!( - driver.did.last(), - Some(&Did::GaveUp(GaveUp::LinkNeverTrained)), - "{:?}", - driver.did - ); -} - -/// A USB2 port whose reset never finishes is still refused the USB2 way: no -/// warm reset is attempted, because the bit does not exist on that port and -/// writing it would be a driver guessing at hardware it was told about. -#[test] -fn a_usb2_port_is_never_warm_reset() { - let mut port = FakePort::empty(ResetBehaviour::Never); - let mut driver = Driver::new().speaking(Protocol::Usb2); - port.attach(); - driver - .run_to(&mut port, 0, DEBOUNCE_NS + 3 * RESET_DEADLINE_NS, PASS) - .unwrap(); - assert_eq!(driver.resets(), [Reset::Hot], "{:?}", driver.did); - assert_eq!( - driver.did.last(), - Some(&Did::GaveUp(GaveUp::ResetNeverFinished)), - "{:?}", - driver.did - ); -} - -/// **The other way a USB3 hot reset fails** (§4.19.5): it *completes* — PRC -/// set — with the port disabled and the link at RxDetect; §4.19.5.1's answer -/// is the same warm reset the deadline shape gets. -#[test] -fn a_reset_that_completes_failed_is_warm_reset() { - let mut port = FakePort::occupied(ResetBehaviour::FailsTheBusReset { warm_works: true }); - let mut driver = Driver::new().speaking(Protocol::Usb3); - driver - .run_to(&mut port, 0, 4 * DEBOUNCE_NS + RESET_DEADLINE_NS, PASS) - .unwrap(); - - assert_eq!( - driver.resets(), - [Reset::Hot, Reset::Warm], - "the completed failure was not escalated to a warm reset: {:?}", - driver.did - ); - assert_eq!(driver.enumerations(), 1, "the port never came up: {:?}", driver.did); - assert!(driver.attached()); - // Exactly once: the recovery must not pay a phantom replug teardown for - // the connect edge its own retrain raised. - assert!( - !driver.did.iter().any(|d| matches!(d, Did::ToreDown(_))), - "the warm reset's own connect edge was read as a replug: {:?}", - driver.did - ); -} - -/// The same scenario with the device **pulled while the warm retrain runs**. -/// -/// The port above is the one place CCS reads zero with a device still in it, so -/// this is the only window where "pulled" and "not detected" are different -/// facts. §4.19.5.1's retrain then finds nothing, and nothing may come up on it. -#[test] -fn a_device_pulled_during_the_warm_retrain_is_not_enumerated() { - let mut port = FakePort::occupied(ResetBehaviour::FailsTheBusReset { warm_works: true }); - let mut driver = Driver::new().speaking(Protocol::Usb3); - let mut pulled_at = None; - let mut now = 0; - while now < 4 * DEBOUNCE_NS + RESET_DEADLINE_NS { - driver.pump(&mut port, now).unwrap(); - // In the pump that wrote WPR, before the port's own clock completes it. - if pulled_at.is_none() && driver.resets() == [Reset::Hot, Reset::Warm] { - port.detach(); - pulled_at = Some(now); - } - now += PASS; - } - - assert!(pulled_at.is_some(), "the warm reset was never reached: {:?}", driver.did); - assert!( - !port.read().connected(), - "the pull left the port reading connected: PORTSC {:#010x}", - port.raw() - ); - assert_eq!( - driver.enumerations(), - 0, - "a device that was pulled mid-retrain was enumerated: {:?}", - driver.did - ); - assert_eq!( - driver.did.last(), - Some(&Did::ToreDown(Gone::Disconnected)), - "the pull did not end in a disconnect teardown: {:?}", - driver.did - ); - assert!(!driver.attached(), "the port is attached with nothing in it: {:?}", driver.did); -} - -/// The same retrain, with the device **pulled after the step that saw it -/// complete and before the enumeration's first read**. The retrain's connect -/// flag is still set, so the pull raises no event; the enumeration's -/// acknowledge clears it and finds the port disabled. The refusal is what has -/// the port read again. -#[test] -fn a_device_pulled_as_its_enumeration_begins_is_torn_down() { - let mut port = FakePort::occupied(ResetBehaviour::FailsTheBusReset { warm_works: true }); - let mut driver = Driver::new().speaking(Protocol::Usb3).pulled_as_it_enumerates(); - driver - .run_to(&mut port, 0, 4 * DEBOUNCE_NS + RESET_DEADLINE_NS, PASS) - .unwrap(); - - assert_eq!( - driver.did, - [ - Did::Reset(Reset::Hot), - Did::Reset(Reset::Warm), - Did::Enumerated { slot: None, trained: false }, - Did::ToreDown(Gone::Disconnected), - ], - "the pull was not refused before Enable Slot and then taken down" - ); - assert!(driver.acts.is_empty(), "a command was spent on a port that read disabled: {:?}", driver.acts); - assert!(!driver.attached(), "the port is attached with nothing in it: {:?}", driver.did); -} - -/// The same failure on a link that will not come back warm either: refused by -/// the name the warm-reset dead end already has. -#[test] -fn a_completed_failure_that_stays_failed_is_refused_by_name() { - let mut port = FakePort::occupied(ResetBehaviour::FailsTheBusReset { warm_works: false }); - let mut driver = Driver::new().speaking(Protocol::Usb3); - driver - .run_to(&mut port, 0, DEBOUNCE_NS + 3 * RESET_DEADLINE_NS, PASS) - .unwrap(); - assert_eq!(driver.resets(), [Reset::Hot, Reset::Warm], "{:?}", driver.did); - assert!( - driver.did.contains(&Did::GaveUp(GaveUp::LinkNeverTrained)), - "{:?}", - driver.did - ); -} - -/// A retrain that completes with the device detected and the port disabled -/// leaves its connect flag set as it gives up. **The port is reset once and -/// not again while its device stays in**, and the pull that ends it is still -/// seen. -#[test] -fn a_port_given_up_on_is_not_reset_again_until_its_device_is_pulled() { - let mut port = FakePort::occupied(ResetBehaviour::RetrainsDisabled); - let mut driver = Driver::new().speaking(Protocol::Usb3); - let held = 2 * RESET_DEADLINE_NS + 20 * DEBOUNCE_NS; - driver.run_to(&mut port, 0, held, PASS).unwrap(); - - assert_eq!( - driver.did, - [Did::Reset(Reset::Hot), Did::Reset(Reset::Warm), Did::GaveUp(GaveUp::LinkNeverTrained)], - "a port given up on was torn down and reset again: {} reset(s) in {held} ns", - driver.resets().len() - ); - assert!(port.read().connected(), "the device left: PORTSC {:#010x}", port.raw()); - - port.detach(); - driver.run_to(&mut port, held, held + 4 * DEBOUNCE_NS, PASS).unwrap(); - assert_eq!( - driver.did.last(), - Some(&Did::ToreDown(Gone::Disconnected)), - "the pull of a port given up on was never seen: {:?}", - driver.did - ); - assert!(!driver.attached(), "{:?}", driver.did); -} - -/// "USB2 protocol ports never fail" (§4.19.5): a controller that completes -/// one disabled anyway is refused by name, never written a WPR it lacks. -#[test] -fn a_usb2_completed_failure_is_refused_not_warm_reset() { - let mut port = FakePort::occupied(ResetBehaviour::FailsTheBusReset { warm_works: false }); - let mut driver = Driver::new().speaking(Protocol::Usb2); - driver - .run_to(&mut port, 0, 4 * DEBOUNCE_NS, PASS) - .unwrap(); - assert_eq!(driver.resets(), [Reset::Hot], "{:?}", driver.did); - assert!( - driver.did.contains(&Did::GaveUp(GaveUp::ResetFailed)), - "{:?}", - driver.did - ); -} diff --git a/lib/xhci/sim/tests/teardown.rs b/lib/xhci/sim/tests/teardown.rs deleted file mode 100644 index 4ab23ce53ce..00000000000 --- a/lib/xhci/sim/tests/teardown.rs +++ /dev/null @@ -1,218 +0,0 @@ -//! What a teardown and an endpoint recovery must do once neither of them is -//! allowed to wait. -//! -//! **These are the states QEMU cannot stage.** A controller that never answers -//! Disable Slot is not a device or a machine property; the window between a -//! device going and the teardown running is 100 ms wide and a `device_del` -//! cannot be aimed inside it; and an endpoint recovery racing its own -//! disconnect needs both edges landed in one debounce with a command already in -//! flight. Every timing here is a constant of the machine or of the simulator, -//! never a literal. - -use toyos_xhci::port::{Gone, Nanos, DEBOUNCE_NS}; -use toyos_xhci::recovery::EndpointState; -use toyos_xhci_sim::driver::{Answers, Broke, Did, Driver, Stuck, ANSWER_DEADLINE_NS}; -use toyos_xhci_sim::hub::{FakePort, ResetBehaviour}; - -/// A reset that finishes quickly, which is every healthy port. -const QUICK: ResetBehaviour = ResetBehaviour::Completes { after: 1_000_000 }; -/// How finely the simulated scheduler samples the clock between wakes. -const PASS: Nanos = 1_000_000; -/// Long enough for a device to be bound and settled. -const SETTLED: Nanos = 4 * DEBOUNCE_NS; - -/// A bound device on a healthy port, with its interrupt endpoint in `state`. -fn bound(state: EndpointState) -> (FakePort, Driver) { - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::new().with_endpoint(state); - driver.run_to(&mut port, 0, SETTLED, PASS).unwrap(); - assert_eq!(driver.enumerations(), 1, "{:?}", driver.did); - (port, driver) -} - -/// Run until the port is no longer bound, and say when that was. -fn released_at(driver: &mut Driver, port: &mut FakePort, from: Nanos, until: Nanos) -> Option { - let mut now = from; - while now < until { - driver.pump(port, now).unwrap(); - if !driver.attached() { - return Some(now); - } - now += PASS; - } - None -} - -#[test] -fn a_teardown_gives_the_slot_and_the_block_back() { - let (mut port, mut driver) = bound(EndpointState::Running); - assert!(driver.block_held()); - - port.detach(); - driver.run_to(&mut port, SETTLED, SETTLED + 4 * DEBOUNCE_NS, PASS).unwrap(); - - assert_eq!(driver.did.last(), Some(&Did::ToreDown(Gone::Disconnected))); - assert_eq!(driver.disabled, 1, "the slot was never given back"); - assert!(!driver.block_held(), "the pool block is still spoken for"); - assert!(!driver.attached()); - assert!(!driver.busy(), "the controller is still owed something"); -} - -/// The controller stops answering, which is what a machine wedged by the device -/// that just left looks like from here. The port must come back on the -/// deadline — and **the loop must have made a pass out of every instant in -/// between**, which is what `run_to` asserts by construction: a driver that -/// spun inside the teardown could not be written in this file. -#[test] -fn a_controller_that_never_answers_still_releases_the_port() { - let (mut port, mut driver) = bound(EndpointState::Running); - driver.answers = Answers::Never; - port.detach(); - - let due = SETTLED + DEBOUNCE_NS + ANSWER_DEADLINE_NS; - driver.run_to(&mut port, SETTLED, due - PASS, PASS).unwrap(); - assert!(driver.busy(), "the teardown stopped waiting early"); - assert!(driver.attached(), "the port was released before its deadline"); - - driver.run_to(&mut port, due - PASS, due + PASS, PASS).unwrap(); - assert_eq!(driver.disabled, 0, "a controller that never answered disabled a slot"); - assert!(!driver.block_held(), "a leaked block is a machine with no disks after two of them"); - assert!(!driver.attached()); -} - -// --------------------------------------------------------------------------- -// The recovery, driven a step per pass. -// --------------------------------------------------------------------------- - -#[test] -fn a_halted_endpoint_is_reset_cleared_and_delivering_again() { - let (mut port, mut driver) = bound(EndpointState::Halted); - driver.endpoint_broke(); - driver.run_to(&mut port, SETTLED, SETTLED + DEBOUNCE_NS, PASS).unwrap(); - - assert_eq!( - driver.commands, - ["Reset Endpoint", "Set TR Dequeue", "CLEAR_FEATURE(ENDPOINT_HALT)"] - ); - assert_eq!(driver.requeues, 1, "the endpoint was never handed its next transfer"); - assert_eq!(driver.let_go, 0); - assert!(driver.attached()); -} - -/// The shape a transfer the driver abandoned on its deadline leaves: the -/// endpoint never halted, so Reset Endpoint would be a Context State Error and -/// CLEAR_FEATURE would ask a device to clear a halt it does not have. -#[test] -fn a_running_endpoint_is_stopped_rather_than_reset() { - let (mut port, mut driver) = bound(EndpointState::Running); - driver.endpoint_broke(); - driver.run_to(&mut port, SETTLED, SETTLED + DEBOUNCE_NS, PASS).unwrap(); - - assert_eq!(driver.commands, ["Stop Endpoint", "Set TR Dequeue"]); - assert_eq!(driver.requeues, 1); -} - -#[test] -fn a_recovery_the_controller_refuses_lets_the_device_go() { - let (mut port, mut driver) = bound(EndpointState::Running); - driver.answers = Answers::Refuses(6); - driver.endpoint_broke(); - driver.run_to(&mut port, SETTLED, SETTLED + DEBOUNCE_NS, PASS).unwrap(); - - assert_eq!(driver.let_go, 1, "{:?}", driver.commands); - assert_eq!(driver.requeues, 0); - // The port stays attached, or the same endpoint is enumerated again every - // debounce for as long as the device stays in it. - assert!(driver.attached()); -} - -// --------------------------------------------------------------------------- -// The disconnect against the recovery, which is the laptop's own event: a -// device pulled while a transfer on its endpoint has just errored. -// --------------------------------------------------------------------------- - -/// The recovery is abandoned and the teardown runs at once, so the port costs -/// one deadline rather than two. -#[test] -fn a_recovery_is_abandoned_when_its_port_goes() { - let (mut port, mut driver) = bound(EndpointState::Halted); - driver.answers = Answers::Never; - driver.endpoint_broke(); - driver.pump(&mut port, SETTLED).unwrap(); - assert!(driver.busy(), "no recovery was started, so this stages nothing"); - - port.detach(); - let end = SETTLED + DEBOUNCE_NS + 3 * ANSWER_DEADLINE_NS; - let at = released_at(&mut driver, &mut port, SETTLED, end).expect("the port was never released"); - - assert_eq!(driver.cancelled, 1, "the recovery outlived the port it was for"); - assert!( - at <= SETTLED + DEBOUNCE_NS + ANSWER_DEADLINE_NS + PASS, - "the port took {} ms to come back, which is more than the one deadline the dead \ - controller costs", - (at - SETTLED) / 1_000_000 - ); -} - -/// **The negative gate for the cancellation.** A recovery left running against -/// a device that has gone holds the one outstanding operation, and the teardown -/// behind it waits out a deadline nothing was ever going to answer. -#[test] -fn gate_a_recovery_that_outlives_its_port_costs_a_second_deadline() { - let (mut port, mut driver) = bound(EndpointState::Halted); - driver = driver.never_cancels(); - driver.answers = Answers::Never; - driver.endpoint_broke(); - driver.pump(&mut port, SETTLED).unwrap(); - - port.detach(); - let end = SETTLED + DEBOUNCE_NS + 3 * ANSWER_DEADLINE_NS; - let at = released_at(&mut driver, &mut port, SETTLED, end).expect("the port was never released"); - - assert_eq!(driver.cancelled, 0, "the flaw did not change the outcome"); - assert!( - at > SETTLED + DEBOUNCE_NS + ANSWER_DEADLINE_NS, - "the port came back at {} ms, so the flaw cost nothing and this gate proves nothing", - (at - SETTLED) / 1_000_000 - ); -} - -/// **The negative gate for the deferral.** A teardown taken while the -/// controller still owes an answer submits a second command over the first, and -/// the slot it is about is one the controller may hand straight back. -#[test] -fn gate_acting_with_an_answer_outstanding_is_caught() { - let (mut port, mut driver) = bound(EndpointState::Halted); - driver = driver.never_cancels().never_defers(); - driver.answers = Answers::Never; - driver.endpoint_broke(); - driver.pump(&mut port, SETTLED).unwrap(); - - port.detach(); - let outcome = driver.run_to(&mut port, SETTLED, SETTLED + 4 * DEBOUNCE_NS, PASS); - assert_eq!( - outcome, - Err(Stuck::Order(Broke::ActedWithAnAnswerOutstanding)), - "the flaw did not change the outcome: {:?}", - driver.did - ); -} - -/// **The negative gate for the teardown's order.** The pool block goes back -/// while the endpoint contexts inside it are still the controller's to write, -/// which is what the next device's transfer ring lands on top of. -#[test] -fn gate_freeing_the_block_before_the_slot_is_caught() { - let (mut port, mut driver) = bound(EndpointState::Running); - driver = driver.frees_blocks_early(); - driver.answers = Answers::Never; - port.detach(); - - let outcome = driver.run_to(&mut port, SETTLED, SETTLED + 4 * DEBOUNCE_NS, PASS); - assert_eq!( - outcome, - Err(Stuck::Order(Broke::ReleasedABlockWithItsSlotOutstanding)), - "the flaw did not change the outcome: {:?}", - driver.did - ); -} diff --git a/lib/xhci/sim/tests/teeth.rs b/lib/xhci/sim/tests/teeth.rs deleted file mode 100644 index 704e88cae04..00000000000 --- a/lib/xhci/sim/tests/teeth.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! The check the negative gates themselves need. -//! -//! A gate asserts what a *flawed* machine does. If the sound machine did the -//! same thing the gate would pass either way and prove nothing — so every flaw -//! is required here to change an outcome this simulator can see. Refactor the -//! machine until a flaw stops being reachable and this goes red, which is what -//! it is for. - -use toyos_xhci::port::{Flaw, DEBOUNCE_NS, RESET_DEADLINE_NS}; -use toyos_xhci_sim::driver::Driver; -use toyos_xhci_sim::hub::{FakePort, ResetBehaviour}; - -const QUICK: ResetBehaviour = ResetBehaviour::Completes { after: 1_000_000 }; -const PASS: u64 = 1_000_000; - -/// Everything one machine does across the two scenarios that between them -/// touch every decision a flaw can spoil. -fn fingerprint(flaw: Flaw) -> String { - let mut out = String::new(); - - // A healthy port: plug, settle, replug inside a debounce, unplug. - let mut port = FakePort::occupied(QUICK); - let mut driver = Driver::with_flaw(flaw); - let r = driver.run_to(&mut port, 0, 4 * DEBOUNCE_NS, PASS); - let mut now = 4 * DEBOUNCE_NS; - if r.is_ok() { - port.replug(); - if driver.run_to(&mut port, now, now + 6 * DEBOUNCE_NS, PASS).is_ok() { - now += 6 * DEBOUNCE_NS; - port.detach(); - let _ = driver.run_to(&mut port, now, now + 6 * DEBOUNCE_NS, PASS); - } - } - out.push_str(&format!("healthy={:?} err={:?};", driver.did, r.err())); - - // A port whose reset never finishes. - let mut port = FakePort::empty(ResetBehaviour::Never); - let mut driver = Driver::with_flaw(flaw); - port.attach(); - let r = driver.run_to(&mut port, 0, DEBOUNCE_NS + 4 * RESET_DEADLINE_NS, PASS); - out.push_str(&format!("dead={:?} err={:?}", driver.did, r.err())); - out -} - -#[test] -fn every_flaw_changes_an_outcome() { - let sound = fingerprint(Flaw::None); - for flaw in [ - Flaw::IgnoreConnectChange, - Flaw::AcknowledgeBeforeDeciding, - Flaw::WriteBackWhatWasRead, - Flaw::RestartDebounce, - Flaw::NoResetDeadline, - ] { - assert_ne!( - fingerprint(flaw), - sound, - "{flaw:?} is not a defect this simulator can see, so the gate that stages it \ - would pass against a machine with the defect in it" - ); - } -} diff --git a/lib/xhci/src/call.rs b/lib/xhci/src/call.rs deleted file mode 100644 index e717af1cc61..00000000000 --- a/lib/xhci/src/call.rs +++ /dev/null @@ -1,546 +0,0 @@ -//! What one disk call may still spend on a controller once its transport has -//! broken, rung by rung. -//! -//! A break is followed, inside the same call, by the recovery ladder -//! ([`crate::ladder`]): commands, control requests, a port reset's settle and a -//! verification, each a spin with its own timeout — and the caller holds the -//! controller's lock with interrupts off for all of them. Summed, those -//! timeouts are no bound anyone stated. [`AfterBreak`] is the bound, and it is -//! one per rung: [`Bounds`] names what the wait that broke and each rung may -//! spend, the call ends at their sum, and **every wait is clipped to where the -//! rungs still ahead of it begin**. So a rung is never entered with less than -//! its own bound, whatever the rungs and the re-issued commands before it -//! spent, and the last rung — the one that leaves the device reset — always -//! runs. -//! -//! **A command sent again after a rung that took is the call's, not the -//! operation's** ([`AfterBreak::issue`]). The caller's operation budget decides -//! whether a command is *started*; the wait that broke may have spent all of it, -//! and a recovery that took with nothing left to send the command again on -//! would fail an operation whose device had just answered. So the command that -//! broke goes out again whenever its window has room, and that window holds -//! everything the rung that took left of its own (the proof is the -//! `every_path_through_a_call_ends_inside_its_bounds` walk). -//! -//! **A controller that left a command unanswered is sent no other in the same -//! call.** Its command ring is one queue: whatever follows waits behind the -//! command it did not answer, and costs a whole timeout to say so again. A -//! port reset is a register write and is still made. - -use crate::ladder::Rung; - -/// Nanoseconds since boot. -pub type Nanos = u64; - -/// Why a step was not taken. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum NotTaken { - /// What this part of the call may spend is spent. - Spent, - /// An earlier command of this call got no answer. - ControllerSilent, -} - -impl core::fmt::Display for NotTaken { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str(match self { - Self::Spent => "the bound on this part of the call is spent", - Self::ControllerSilent => "the controller left an earlier command of this call unanswered", - }) - } -} - -/// Why a command was not sent. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum NotIssued { - /// The operation's own budget is spent, and this command would start - /// something new in it. - Operation, - /// The call's bound refuses it. - Call(NotTaken), -} - -/// What each part of a call whose transport broke may spend. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Bounds { - /// The wait that broke, and every command re-issued after a rung that - /// took, together. - pub wait: Nanos, - pub class_reset: Nanos, - pub port_reset: Nanos, - pub offline: Nanos, -} - -/// What one disk call may spin for once its transport has broken, part by part: -/// the kernel's, and the one declaration a test times a call against. -/// -/// A rung's bound is what its commands, requests and reset cost a device that -/// answers, and the rest of it is how long a TEST UNIT READY nothing answers is -/// waited for. `wait` is the driver's own timeout on one command, which the -/// kernel asserts. -pub const AFTER_BREAK: Bounds = Bounds { - wait: 2_000_000_000, - class_reset: 750_000_000, - port_reset: 1_500_000_000, - offline: 500_000_000, -}; - -impl Bounds { - /// Everything the call may spin for from the start of the wait that broke. - pub const fn whole(&self) -> Nanos { - self.wait + self.class_reset + self.port_reset + self.offline - } - - const fn of(&self, rung: Rung) -> Nanos { - match rung { - Rung::ClassReset => self.class_reset, - Rung::PortReset => self.port_reset, - Rung::Offline => self.offline, - } - } - - /// What the rungs above `rung` are owed. - const fn above(&self, rung: Rung) -> Nanos { - match rung { - Rung::ClassReset => self.port_reset + self.offline, - Rung::PortReset => self.offline, - Rung::Offline => 0, - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -struct Open { - bounds: Bounds, - /// Where the call ends: the start of the wait that broke plus - /// [`Bounds::whole`]. - call_ends: Nanos, - /// Where a wait started now is clipped. - window_ends: Nanos, -} - -/// One call's bounds after a break; [`Self::CLOSED`] between calls and in a -/// call whose transport has not broken, where it clips and refuses nothing. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct AfterBreak { - open: Option, - silent: bool, - /// A rung took, or a held disk came back, and the command it was for has - /// not gone out again yet. - again: bool, -} - -impl AfterBreak { - pub const CLOSED: Self = Self { open: None, silent: false, again: false }; - - /// The transport broke on a wait that began at `wait_began`. The first - /// break of a call opens it and a later one does not extend it. Until a - /// rung is entered every wait ends where the first rung's bound begins. - pub fn open(&mut self, wait_began: Nanos, bounds: Bounds) { - if self.open.is_none() { - let call_ends = wait_began.saturating_add(bounds.whole()); - let window_ends = call_ends - bounds.of(Rung::ClassReset) - bounds.above(Rung::ClassReset); - self.open = Some(Open { bounds, call_ends, window_ends }); - } - } - - /// `rung` begins at `now`: its waits get its own bound, and never reach - /// into what the rungs above it are owed. - pub fn enter(&mut self, rung: Rung, now: Nanos) { - if let Some(open) = &mut self.open { - let own = now.saturating_add(open.bounds.of(rung)); - open.window_ends = own.min(open.call_ends - open.bounds.above(rung)); - } - } - - /// `rung` took and the caller's command goes out again: its waits end - /// where the rungs above `rung` begin. - pub fn took(&mut self, rung: Rung) { - if let Some(open) = &mut self.open { - open.window_ends = open.call_ends - open.bounds.above(rung); - self.again = true; - } - } - - /// Whether the command about to go out at `now` may, in an operation whose - /// own budget ends at `operation_ends`. - /// - /// The first command to go out after a rung took, or after a held disk came - /// back, is the one the break was in, sent again: only the call's window - /// decides it, since the operation's budget was spent on the break it - /// recovered from. Every other command starts something new in the - /// operation, and needs both. - pub fn issue(&mut self, now: Nanos, operation_ends: Nanos) -> Result<(), NotIssued> { - let again = core::mem::take(&mut self.again); - if !again && now >= operation_ends { - return Err(NotIssued::Operation); - } - self.request(now).map_err(NotIssued::Call) - } - - /// When a wait starting at `now`, with `own` as its own timeout, gives up. - pub fn wait_ends(&self, now: Nanos, own: Nanos) -> Nanos { - let own = now.saturating_add(own); - self.open.map_or(own, |open| own.min(open.window_ends)) - } - - /// How long a wait starting at `now` may spin: nothing, once its window - /// has ended, however long ago that was. - pub fn wait_left(&self, now: Nanos, own: Nanos) -> Nanos { - self.wait_ends(now, own).saturating_sub(now) - } - - /// Whether a wait that gave up at `now` was cut short by its window and - /// not by its own timeout. - pub fn cut(&self, began: Nanos, now: Nanos, own: Nanos) -> bool { - self.open.is_some_and(|open| now >= open.window_ends && now < began.saturating_add(own)) - } - - /// Whether a command may go to the controller at `now`. - pub fn command(&self, now: Nanos) -> Result<(), NotTaken> { - if self.silent { - return Err(NotTaken::ControllerSilent); - } - self.request(now) - } - - /// Whether anything that is waited for may start at `now`. - pub fn request(&self, now: Nanos) -> Result<(), NotTaken> { - match self.open { - Some(open) if now >= open.window_ends => Err(NotTaken::Spent), - _ => Ok(()), - } - } - - /// A command got no answer. - pub fn unanswered(&mut self) { - self.silent = true; - } - - /// The disk this call is on is held for its device to come back - /// ([`crate::identity`]), as found at `now`: until when the call may wait - /// for the verdict. A call whose transport had not broken opens here, as if - /// a wait had broken now. - /// - /// **The hold is part of the call, not an addition to it.** It ends where - /// the last rung begins, and the command sent again on a device that came - /// back is clipped there too, as after a port rung that took: a break of it - /// climbs on what is left, and the call still ends at its bounds' sum. The - /// wait for a returning device never binds it; that is the port machine's, - /// wherever it runs. - pub fn hold(&mut self, now: Nanos, bounds: Bounds) -> Nanos { - self.open(now, bounds); - self.took(Rung::PortReset); - self.open.map_or(now, |open| open.window_ends) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const SECOND: Nanos = 1_000_000_000; - const OWN: Nanos = 2 * SECOND; - const BOUNDS: Bounds = Bounds { - wait: 2 * SECOND, - class_reset: SECOND, - port_reset: 3 * SECOND / 2, - offline: SECOND / 4, - }; - const RUNGS: [Rung; 3] = [Rung::ClassReset, Rung::PortReset, Rung::Offline]; - - #[test] - fn a_call_whose_transport_has_not_broken_is_clipped_and_refused_nothing() { - let mut call = AfterBreak::CLOSED; - assert_eq!(call.wait_ends(10 * SECOND, OWN), 12 * SECOND); - assert_eq!(call.wait_left(10 * SECOND, OWN), OWN); - assert_eq!(call.command(u64::MAX), Ok(())); - assert_eq!(call.request(u64::MAX), Ok(())); - call.enter(Rung::Offline, 0); - call.took(Rung::ClassReset); - assert_eq!(call, AfterBreak::CLOSED, "a rung of a call that never broke opens nothing"); - } - - /// Every rung is spun to the end of its window, the re-issues between them - /// too, and each rung is still entered with the whole of its own bound. - #[test] - fn a_rung_is_never_starved_by_what_was_spent_before_it() { - let began = 100 * SECOND; - let mut call = AfterBreak::CLOSED; - // The wait that broke ran out its own timeout. - let mut now = began + OWN; - call.open(began, BOUNDS); - for rung in RUNGS { - call.enter(rung, now); - assert_eq!(call.wait_left(now, u64::MAX), BOUNDS.of(rung), "{rung:?} is owed its whole bound"); - assert_eq!(call.command(now), Ok(())); - // Spun to the end of its window. - now = call.wait_ends(now, u64::MAX); - assert_eq!(call.request(now), Err(NotTaken::Spent)); - // It is called taken, and the command that goes out again spins - // for all it may. - call.took(rung); - now = call.wait_ends(now, u64::MAX); - } - assert_eq!(now, began + BOUNDS.whole(), "and the call ends where its bounds sum to"); - } - - /// A rung entered early keeps its own bound and no more: what it leaves - /// goes to nobody. - #[test] - fn a_rung_entered_early_gets_its_own_bound_and_not_the_calls() { - let mut call = AfterBreak::CLOSED; - call.open(0, BOUNDS); - call.enter(Rung::ClassReset, 0); - assert_eq!(call.wait_ends(0, u64::MAX), BOUNDS.class_reset); - call.enter(Rung::PortReset, SECOND / 2); - assert_eq!(call.wait_ends(SECOND / 2, u64::MAX), SECOND / 2 + BOUNDS.port_reset); - call.enter(Rung::Offline, SECOND); - assert_eq!(call.wait_ends(SECOND, u64::MAX), SECOND + BOUNDS.offline); - } - - /// A call that begins on a device already a rung up — its breaks are - /// counted across calls — enters that rung first and finds it whole. - #[test] - fn a_call_that_enters_the_ladder_a_rung_up_finds_that_rung_whole() { - for (at, rung) in RUNGS.into_iter().enumerate() { - let mut call = AfterBreak::CLOSED; - call.open(0, BOUNDS); - let now = OWN; - call.enter(rung, now); - assert_eq!(call.wait_left(now, u64::MAX), BOUNDS.of(rung), "rung {at}"); - } - } - - /// A command re-issued after a rung that took may not reach into the rungs - /// its own break would climb. - #[test] - fn a_command_sent_again_is_clipped_to_where_the_next_rung_begins() { - let mut call = AfterBreak::CLOSED; - call.open(0, BOUNDS); - call.enter(Rung::ClassReset, 0); - call.took(Rung::ClassReset); - assert_eq!(call.wait_ends(0, u64::MAX), BOUNDS.whole() - BOUNDS.port_reset - BOUNDS.offline); - call.took(Rung::PortReset); - assert_eq!(call.wait_ends(0, u64::MAX), BOUNDS.whole() - BOUNDS.offline); - // Inside that, a wait keeps its own timeout. - assert_eq!(call.wait_ends(SECOND, SECOND), 2 * SECOND); - } - - /// A silent controller is refused by that name in every rung, with bound - /// left: the next command would queue behind the one it did not answer. A - /// register wait — the port reset's — is not a command. - #[test] - fn a_silent_controller_is_sent_no_command_in_any_rung_and_its_port_is_still_reset() { - let mut call = AfterBreak::CLOSED; - call.open(0, BOUNDS); - call.enter(Rung::ClassReset, 0); - call.unanswered(); - for rung in RUNGS { - call.enter(rung, SECOND); - assert_eq!(call.command(SECOND), Err(NotTaken::ControllerSilent), "{rung:?}"); - assert_eq!(call.request(SECOND), Ok(()), "{rung:?}"); - } - } - - #[test] - fn a_second_break_in_the_same_call_does_not_extend_it() { - let mut call = AfterBreak::CLOSED; - call.open(0, BOUNDS); - call.open(3 * SECOND, BOUNDS); - call.enter(Rung::Offline, 0); - call.took(Rung::Offline); - assert_eq!(call.wait_ends(0, u64::MAX), BOUNDS.whole()); - } - - /// What a disk call does once its transport broke or its disk was found - /// held, as far as time goes: every wait, rung, hold and command sent again - /// the driver makes is one of these. - #[derive(Clone, Copy, Debug)] - enum Does { - Enter(Rung), - Took(Rung), - /// Waits for a returning device to the end of what the hold allows. - Hold, - /// A wait that spins to the end of its window, whatever its own timeout. - Spin, - } - - const EVERYTHING: [Does; 8] = [ - Does::Enter(Rung::ClassReset), - Does::Enter(Rung::PortReset), - Does::Enter(Rung::Offline), - Does::Took(Rung::ClassReset), - Does::Took(Rung::PortReset), - Does::Took(Rung::Offline), - Does::Hold, - Does::Spin, - ]; - - /// Every path a call can take, each step spun to its worst case, ends - /// inside the bounds' sum from where it opened: a break, a ladder, a hold - /// for a device that left, the command sent again on the one that came - /// back, a break of that and the rungs after it, and a hold again. This is - /// the whole of what a call spins for with `IF` clear once it has broken or - /// found its disk held, so it is what the kernel's `CALL_AFTER_BREAK` is - /// held under the TLB-ack tripwire for. - #[test] - fn every_path_through_a_call_ends_inside_its_bounds() { - const STEPS: usize = 6; - let began = 100 * SECOND; - let whole = BOUNDS.whole(); - // Where a call opens: a wait that broke at once, one that ran out its - // own timeout, and a call that found its disk already held. - for start in 0..3 { - let mut path = [0usize; STEPS]; - loop { - let mut call = AfterBreak::CLOSED; - let mut now = began; - match start { - 0 => call.open(began, BOUNDS), - 1 => { - call.open(began, BOUNDS); - now = began + OWN; - } - _ => { - let ends = call.hold(now, BOUNDS); - assert_eq!(ends, began + whole - BOUNDS.offline, "a hold opens a call"); - now = ends; - } - } - let steps = path.map(|s| EVERYTHING[s]); - // The rung the call is inside, as its last step entered it. - let mut inside = None; - for (at, &does) in steps.iter().enumerate() { - let taken = &steps[..=at]; - match does { - Does::Enter(rung) => { - call.enter(rung, now); - inside = Some(rung); - } - Does::Took(rung) => { - let rung_ends = call.wait_ends(now, u64::MAX); - call.took(rung); - // A rung that took inside its own window leaves the - // command sent again at least what it had left. - if inside == Some(rung) && now < rung_ends { - assert!( - call.wait_ends(now, u64::MAX) >= rung_ends, - "start {start}, {taken:?}: the command sent again ends before \ - the rung that took would have" - ); - } - inside = None; - } - Does::Hold => { - let ends = call.hold(now, BOUNDS); - assert!(ends <= began + whole - BOUNDS.offline, "start {start}, {taken:?}"); - now = now.max(ends); - inside = None; - } - Does::Spin => now = now.max(call.wait_ends(now, u64::MAX)), - } - assert!(now <= began + whole, "start {start}, {taken:?}: {now} past {}", began + whole); - assert!(call.wait_ends(now, u64::MAX) <= began + whole, "start {start}, {taken:?}"); - } - // The next path, as a number in base `EVERYTHING.len()`. - let Some(carry) = path.iter().position(|&s| s + 1 < EVERYTHING.len()) else { break }; - path[carry] += 1; - path[..carry].fill(0); - } - } - } - - /// The command sent again on a device that came back may not reach into - /// what the last rung is owed, and a break of it still finds that rung. - #[test] - fn a_command_sent_again_after_a_hold_leaves_the_last_rung_its_bound() { - let mut call = AfterBreak::CLOSED; - call.open(0, BOUNDS); - call.enter(Rung::PortReset, OWN); - // The device left at once, and came back at once. - let ends = call.hold(OWN, BOUNDS); - assert_eq!(ends, BOUNDS.whole() - BOUNDS.offline); - assert_eq!(call.wait_ends(OWN, u64::MAX), ends, "the command sent again spins to the hold's end"); - // It broke there: the rungs below the last find nothing left. - call.enter(Rung::ClassReset, ends); - assert_eq!(call.request(ends), Err(NotTaken::Spent)); - call.enter(Rung::Offline, ends); - assert_eq!(call.wait_left(ends, u64::MAX), BOUNDS.offline, "and the last one its whole bound"); - assert_eq!(call.wait_ends(ends, u64::MAX), BOUNDS.whole()); - } - - /// A wait inside its window keeps its own timeout, and one that ends on it - /// is not called cut. - #[test] - fn a_wait_is_cut_only_when_its_window_ended_it() { - let mut call = AfterBreak::CLOSED; - call.open(0, BOUNDS); - call.enter(Rung::PortReset, 0); - assert_eq!(call.wait_ends(0, SECOND), SECOND); - assert!(!call.cut(0, SECOND, SECOND)); - assert_eq!(call.wait_ends(SECOND, OWN), BOUNDS.port_reset); - assert!(call.cut(SECOND, BOUNDS.port_reset, OWN)); - assert!(!AfterBreak::CLOSED.cut(0, OWN, OWN)); - } - - const MS: Nanos = 1_000_000; - - /// A stick whose first READ(10) was not taken for the whole of its wait: the - /// wait spent the operation's two seconds, the class reset's TEST UNIT READY - /// babbled, the port reset took, and the READ goes out again with its whole - /// own timeout — on the kernel's own bounds, at the times the T14 recorded. - #[test] - fn a_read_whose_wait_spent_the_operation_goes_out_again_after_the_port_reset_took() { - let own = AFTER_BREAK.wait; - let operation_ends = 600 * MS + 2 * SECOND; - let wait_began = 602 * MS; - let mut call = AfterBreak::CLOSED; - assert_eq!(call.issue(wait_began, operation_ends), Ok(()), "the READ goes out"); - - let broke = wait_began + own; - call.open(wait_began, AFTER_BREAK); - call.enter(Rung::ClassReset, broke); - // Its TEST UNIT READY out of step at once: the next rung. - call.enter(Rung::PortReset, broke); - let took = 2_757 * MS; - assert_eq!(call.request(took), Ok(()), "the port rung took inside its window"); - call.took(Rung::PortReset); - - assert!(took >= operation_ends, "the operation's budget is spent"); - assert_eq!(call.issue(took, operation_ends), Ok(()), "and the READ still goes out again"); - assert_eq!(call.wait_left(took, own), own, "with the whole of its own timeout"); - - // What goes out after it starts something new: the operation's budget - // refuses it, by that name. - assert_eq!(call.issue(took, operation_ends), Err(NotIssued::Operation)); - - // It broke too, on its whole wait: the last rung is still whole, and the - // call ends inside the bounds' sum. - let broke_again = took + own; - call.enter(Rung::Offline, broke_again); - assert_eq!(call.wait_left(broke_again, u64::MAX), AFTER_BREAK.offline); - assert!(call.wait_ends(broke_again, u64::MAX) <= wait_began + AFTER_BREAK.whole()); - } - - /// Only the command sent again is exempt from the operation's budget, and - /// only after a rung that took: a call whose transport never broke, or whose - /// rung did not take, sends nothing past it. - #[test] - fn nothing_but_the_command_sent_again_outlives_the_operations_budget() { - let mut call = AfterBreak::CLOSED; - assert_eq!(call.issue(2 * SECOND, 2 * SECOND), Err(NotIssued::Operation)); - call.open(0, BOUNDS); - call.enter(Rung::ClassReset, OWN); - assert_eq!(call.issue(OWN, SECOND), Err(NotIssued::Operation), "no rung has taken"); - call.took(Rung::ClassReset); - assert_eq!(call.issue(OWN, SECOND), Ok(())); - assert_eq!(call.issue(OWN, SECOND), Err(NotIssued::Operation), "and only once"); - // A held disk that came back is sent the operation again the same way. - let ends = call.hold(OWN, BOUNDS); - assert_eq!(call.issue(OWN, SECOND), Ok(())); - // The call's own bound still refuses it, whichever it is. - call.took(Rung::PortReset); - assert_eq!(call.issue(ends, SECOND), Err(NotIssued::Call(NotTaken::Spent))); - } -} diff --git a/lib/xhci/src/enumerate.rs b/lib/xhci/src/enumerate.rs deleted file mode 100644 index 00fc71c1722..00000000000 --- a/lib/xhci/src/enumerate.rs +++ /dev/null @@ -1,427 +0,0 @@ -//! Everything between a port that has finished its reset and a device the -//! driver can bind, as an order and the two places it branches. -//! -//! The shape is [`crate::recovery`]'s and for the same reason: the sequence has -//! two drivers. A boot scan runs it to the end in place, because there is no -//! scheduler yet to give a pass back to; a hot plug runs one act per scheduler -//! pass, because there is, and `poll_if_pending` is at the top of every one of -//! them. A second implementation for the second driver would be two orders to -//! keep in step, and the one that only runs on hardware nobody has plugged into -//! is the one that would rot. -//! -//! **What is here is the order and nothing else.** Which endpoints an interface -//! offered, what a slot id is, where a ring lives — none of that decides what -//! comes next, so none of it is here. - -/// EP0's Max Packet Size before the device has been asked, and `None` for a -/// speed this driver has no encoding for. -/// -/// Low, High and SuperSpeed each fix it — 8, 64 and 512 — and a device of that -/// speed may report nothing else. **Full Speed does not.** `bMaxPacketSize0` is -/// 8, 16, 32 or 64 there, and it is not known until the first eight bytes of -/// the device descriptor have been read, which is itself a transfer over EP0. -/// Every device answers the descriptor's first eight bytes in one packet (USB -/// 2.0 §5.5.3), so 8 is what Address Device carries and what Evaluate Context -/// replaces once the device has said (xHCI 1.2 §4.3, step 7). -/// -/// The old table answered 64 for Full Speed and **8 for everything it did not -/// recognise**, so a SuperSpeedPlus port — speed 5, 6 or 7, which is every -/// Gen 2 and every two-lane link — was addressed with a 64-fold undersized -/// control endpoint and no line said so. -pub fn initial_ep0_packet(speed: u8) -> Option { - match speed { - 1 | 2 => Some(8), - 3 => Some(64), - 4..=7 => Some(512), - _ => None, - } -} - -/// What `bMaxPacketSize0` means at this speed, or `None` when the device named -/// a size a device of that speed does not have. -/// -/// SuperSpeed states the *exponent*: the byte is 9 and the size is 512 (USB 3.2 -/// §9.6.1). Everything below it states the size itself, and only Full Speed has -/// a choice to state. -pub fn ep0_packet_from_descriptor(speed: u8, stated: u8) -> Option { - match (speed, stated) { - (1, 8 | 16 | 32 | 64) => Some(stated as u16), - (2, 8) => Some(8), - (3, 64) => Some(64), - (4..=7, 9) => Some(512), - _ => None, - } -} - -/// A command the enumeration issues on the command ring. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Command { - EnableSlot, - AddressDevice, - /// The command that changes exactly EP0's Max Packet Size on a device that - /// is already addressed (xHCI 1.2 §4.6.7). Configure Endpoint would work on - /// a device that is not yet configured and says far more than is meant. - EvaluateEp0, - ConfigureEndpoint, - /// Reset Endpoint on DCI 1, which is what takes the default control pipe - /// out of Halted after the device stalled a request the sequence went on - /// from (xHCI 1.2 §4.6.8). - ResetEp0, - /// …and Set TR Dequeue Pointer for the same endpoint, because the - /// controller's dequeue pointer is still on the TRB that stalled: without - /// it the next control transfer re-runs the stalled one (xHCI 1.2 §4.6.10). - SetEp0Dequeue, -} - -/// A control request the enumeration issues on EP0. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Request { - /// GET_DESCRIPTOR(Device) for `want` bytes. **Two sizes and not one**: the - /// eighth byte is `bMaxPacketSize0`, and on a full-speed device it is what - /// decides whether a longer read can be transferred at all — so the prefix - /// is read at a size every device of the speed can answer at. - DeviceDescriptor { want: u16 }, - ConfigDescriptor, - SetConfiguration, - /// SET_PROTOCOL(boot), which only an interface that has a boot protocol - /// has: asking a tablet for one is a request it may stall for. - SetProtocol, -} - -/// What the driver does next. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Act { - Command(Command), - Request(Request), -} - -/// What a configuration descriptor offered, as far as *the order of what is -/// left* depends on it. -/// -/// Which endpoints, which interface number and which packet sizes are the -/// driver's, because nothing here decides anything from them. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Function { - /// A HID interface with a boot protocol to select. - BootHid, - /// A HID interface with none — a tablet, which reports in its own format. - Hid, - Msc, -} - -/// What the driver learnt from the act it just performed, where the order of -/// what is left depends on it. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Learnt { - /// Nothing the order depends on. Every act but two produces this. - Nothing, - /// The device states an EP0 packet size other than the one Address Device - /// carried, so the controller has to be told. - Ep0PacketWrong, - /// The configuration descriptor named a function this driver can bind. - Function(Function), - /// The device stalled the act, and the sequence goes on regardless — which - /// is a decision only [`Request::SetProtocol`] has. The stall left EP0 - /// halted, so what is owed before the next act is EP0's own recovery. - Stalled, -} - -/// Where the sequence goes after the act that has just completed. -/// -/// A sum and not another [`Act`] variant, because the two ends are terminal and -/// this is what makes asking a finished sequence for another act unrepresentable -/// rather than a state to be handled. [`crate::recovery`] needs no such thing: -/// its terminal is a state the endpoint is *in*, so answering it twice says the -/// same true thing, and answering `Bind` twice would bind a device twice. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Next { - /// Do this, then come back with what it produced. - Act(Enumeration, Act), - /// Everything every device needs is done. What is left is the class bind, - /// which is the driver's. - Bind, - /// The configuration held no interface this driver can drive. The slot is - /// still the controller's and only Disable Slot gives it back. - Refuse, -} - -/// Which act of the sequence is outstanding. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum At { - Slot, - Address, - Prefix, - Ep0, - Descriptor, - Config, - Configuration, - Protocol, - Ep0Reset, - Ep0Dequeue, - Endpoints, -} - -/// One device's enumeration, as the acts still owed. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Enumeration { - at: At, - /// Whether this device has a boot protocol to select, which is the only - /// thing the later acts still need to know about what the configuration - /// said. - boot_protocol: bool, -} - -impl Enumeration { - /// The first act every device needs, whatever it turns out to be. - pub fn begin() -> (Self, Act) { - ( - Self { at: At::Slot, boot_protocol: false }, - Act::Command(Command::EnableSlot), - ) - } - - /// The act just performed succeeded, and this is what it taught. What is - /// owed next. - pub fn completed(mut self, learnt: Learnt) -> Next { - let (at, act) = match self.at { - At::Slot => (At::Address, Act::Command(Command::AddressDevice)), - At::Address => { - (At::Prefix, Act::Request(Request::DeviceDescriptor { want: 8 })) - } - At::Prefix if learnt == Learnt::Ep0PacketWrong => { - (At::Ep0, Act::Command(Command::EvaluateEp0)) - } - At::Prefix | At::Ep0 => { - (At::Descriptor, Act::Request(Request::DeviceDescriptor { want: 18 })) - } - At::Descriptor => (At::Config, Act::Request(Request::ConfigDescriptor)), - At::Config => { - let Learnt::Function(function) = learnt else { return Next::Refuse }; - self.boot_protocol = function == Function::BootHid; - (At::Configuration, Act::Request(Request::SetConfiguration)) - } - At::Configuration if self.boot_protocol => { - (At::Protocol, Act::Request(Request::SetProtocol)) - } - // A tolerated stall halts EP0 at the controller, and the device - // clears its own half on the next SETUP (USB 2.0 §8.5.3.4). So what - // is owed is the controller's two commands and no packet on the bus - // — which is why this branch is here and not a `Request`. - At::Protocol if learnt == Learnt::Stalled => { - (At::Ep0Reset, Act::Command(Command::ResetEp0)) - } - At::Ep0Reset => (At::Ep0Dequeue, Act::Command(Command::SetEp0Dequeue)), - At::Configuration | At::Protocol | At::Ep0Dequeue => { - (At::Endpoints, Act::Command(Command::ConfigureEndpoint)) - } - At::Endpoints => return Next::Bind, - }; - Next::Act(Self { at, ..self }, act) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Sized past the longest route so a sequence that grew one runs off it - /// rather than being silently truncated into a passing comparison. - const LONGEST: usize = 12; - - /// A route, as the acts it produced and how it ended. `Copy` so the - /// assertions below can compare slices of it without a heap this crate does - /// not have. - struct Route { - acts: [Act; LONGEST], - n: usize, - end: Next, - } - - impl Route { - fn acts(&self) -> &[Act] { - &self.acts[..self.n] - } - - fn count(&self, act: Act) -> usize { - self.acts().iter().filter(|a| **a == act).count() - } - } - - /// Every act a device that answers `learnt` at each branch produces, in - /// order, and how the sequence ended. - fn route(mut learnt: impl FnMut(Act) -> Learnt) -> Route { - let (mut seq, first) = Enumeration::begin(); - let mut acts = [first; LONGEST]; - let mut n = 1; - loop { - match seq.completed(learnt(acts[n - 1])) { - Next::Act(next, act) => { - assert!(n < LONGEST, "a sequence that does not terminate: {acts:?}"); - seq = next; - acts[n] = act; - n += 1; - } - end => return Route { acts, n, end }, - } - } - } - - /// Answer every branch the way a keyboard whose EP0 packet size the - /// controller was told correctly does. - fn keyboard(act: Act) -> Learnt { - match act { - Act::Request(Request::ConfigDescriptor) => Learnt::Function(Function::BootHid), - _ => Learnt::Nothing, - } - } - - #[test] - fn a_boot_keyboard_is_addressed_described_configured_and_bound() { - let route = route(keyboard); - assert_eq!(route.end, Next::Bind); - assert_eq!( - route.acts(), - [ - Act::Command(Command::EnableSlot), - Act::Command(Command::AddressDevice), - Act::Request(Request::DeviceDescriptor { want: 8 }), - Act::Request(Request::DeviceDescriptor { want: 18 }), - Act::Request(Request::ConfigDescriptor), - Act::Request(Request::SetConfiguration), - Act::Request(Request::SetProtocol), - Act::Command(Command::ConfigureEndpoint), - ] - ); - } - - /// The full-speed case Evaluate Context exists for: Address Device carried - /// 8 because that is all a full-speed device is guaranteed to answer at, - /// and the device states 64. - #[test] - fn a_device_that_states_another_packet_size_has_its_ep0_evaluated() { - let route = route(|act| match act { - Act::Request(Request::DeviceDescriptor { want: 8 }) => Learnt::Ep0PacketWrong, - other => keyboard(other), - }); - assert_eq!(route.end, Next::Bind); - assert_eq!(route.acts()[2..5], [ - Act::Request(Request::DeviceDescriptor { want: 8 }), - Act::Command(Command::EvaluateEp0), - Act::Request(Request::DeviceDescriptor { want: 18 }), - ]); - // And exactly once: a second Evaluate Context would be asked from the - // state the first one left. - assert_eq!(route.count(Act::Command(Command::EvaluateEp0)), 1); - } - - /// The one act the sequence goes on from after a stall, and what a stall - /// leaves behind: EP0 halted at the controller, recovered by two commands - /// and no packet on the bus. - #[test] - fn a_stalled_set_protocol_recovers_ep0_before_the_endpoints_are_configured() { - let route = route(|act| match act { - Act::Request(Request::SetProtocol) => Learnt::Stalled, - other => keyboard(other), - }); - assert_eq!(route.end, Next::Bind); - assert_eq!( - route.acts()[6..], - [ - Act::Request(Request::SetProtocol), - Act::Command(Command::ResetEp0), - Act::Command(Command::SetEp0Dequeue), - Act::Command(Command::ConfigureEndpoint), - ] - ); - // Both, and in that order: Reset Endpoint alone leaves the controller's - // dequeue pointer on the TRB that stalled, so the next control transfer - // runs it again. - assert_eq!(route.count(Act::Command(Command::ResetEp0)), 1); - assert_eq!(route.count(Act::Command(Command::SetEp0Dequeue)), 1); - } - - /// …and a device that did not stall pays nothing for it. - #[test] - fn a_device_that_answered_set_protocol_recovers_nothing() { - let route = route(keyboard); - assert_eq!(route.count(Act::Command(Command::ResetEp0)), 0); - assert_eq!(route.count(Act::Command(Command::SetEp0Dequeue)), 0); - } - - /// A disk and a tablet have no boot protocol to select, and asking for one - /// is a request the device may stall for. - #[test] - fn only_an_interface_with_a_boot_protocol_is_asked_for_one() { - for function in [Function::Msc, Function::Hid] { - let route = route(|act| match act { - Act::Request(Request::ConfigDescriptor) => Learnt::Function(function), - _ => Learnt::Nothing, - }); - assert_eq!(route.end, Next::Bind); - assert_eq!( - route.count(Act::Request(Request::SetProtocol)), - 0, - "{function:?} was asked for a boot protocol it has none of: {:?}", - route.acts() - ); - } - } - - /// Every route configures the endpoints exactly once and immediately before - /// the bind. A bind that ran first would put a device's transfer rings in - /// pool memory no endpoint context names yet. - #[test] - fn every_route_configures_its_endpoints_last() { - for function in [Function::BootHid, Function::Hid, Function::Msc] { - let route = route(|act| match act { - Act::Request(Request::ConfigDescriptor) => Learnt::Function(function), - _ => Learnt::Nothing, - }); - assert_eq!(route.acts().last(), Some(&Act::Command(Command::ConfigureEndpoint))); - let configures = route.count(Act::Command(Command::ConfigureEndpoint)); - assert_eq!(configures, 1, "{function:?} issued {configures}: {:?}", route.acts()); - } - } - - /// A device offering nothing this driver binds — a hub, a camera — stops - /// the sequence where the answer is known, rather than configuring a - /// device with no interface behind it. - #[test] - fn a_configuration_with_nothing_to_bind_is_refused_where_it_is_read() { - let route = route(|_| Learnt::Nothing); - assert_eq!(route.end, Next::Refuse); - assert_eq!(route.acts().last(), Some(&Act::Request(Request::ConfigDescriptor))); - } - - /// Address Device carries a packet size and only these speeds have one, so - /// a port that comes up at any other is refused before a slot is spent. - #[test] - fn every_speed_has_one_control_packet_size_or_none() { - let want = [None, Some(8), Some(8), Some(64), Some(512), Some(512), Some(512), Some(512)]; - for (speed, packet) in want.iter().enumerate() { - assert_eq!(initial_ep0_packet(speed as u8), *packet, "speed {speed}"); - } - for speed in 8..=15u8 { - assert_eq!(initial_ep0_packet(speed), None, "speed {speed}"); - } - } - - /// The byte is the *size* below SuperSpeed and the *exponent* at it, and - /// only Full Speed has a choice to state. - #[test] - fn a_stated_packet_size_a_device_of_that_speed_cannot_have_is_refused() { - for stated in [8u8, 16, 32, 64] { - assert_eq!(ep0_packet_from_descriptor(1, stated), Some(stated as u16)); - } - assert_eq!(ep0_packet_from_descriptor(1, 9), None); - assert_eq!(ep0_packet_from_descriptor(2, 8), Some(8)); - assert_eq!(ep0_packet_from_descriptor(2, 64), None); - assert_eq!(ep0_packet_from_descriptor(3, 64), Some(64)); - assert_eq!(ep0_packet_from_descriptor(3, 8), None); - for speed in 4..=7u8 { - assert_eq!(ep0_packet_from_descriptor(speed, 9), Some(512)); - assert_eq!(ep0_packet_from_descriptor(speed, 512u16 as u8), None); - } - assert_eq!(ep0_packet_from_descriptor(0, 8), None); - } -} diff --git a/lib/xhci/src/job.rs b/lib/xhci/src/job.rs deleted file mode 100644 index 330111299a3..00000000000 --- a/lib/xhci/src/job.rs +++ /dev/null @@ -1,474 +0,0 @@ -//! The one operation this controller has submitted and not seen the end of. -//! -//! A driver that may not wait has to be able to say what it is waiting *for*, -//! and to say it in a form a later pass can compare an arriving event against. -//! That is all this is: what ends the wait, when the wait stops being worth -//! having, and the answer once one arrives. -//! -//! **What it is waiting for is always a TRB address**, on the command ring and -//! on a transfer ring alike, because that is the only name an event carries -//! that belongs to one operation and to no other. Every coarser answer this -//! file could give — the next command completion, the next completion on an -//! endpoint, one more of two — is spendable by whatever arrives next, and a -//! driver that stopped waiting for something the device is still going to -//! answer is precisely what produces that arrival. [`Await`] carries it in -//! full. - -use crate::port::Nanos; - -/// What the controller has to produce for an outstanding operation to be over. -/// -/// **Both arms carry the physical address of the TRB whose completion this -/// is**, which is what the event names in its first two dwords: a Command -/// Completion Event names its Command TRB (xHCI 1.2 §6.4.2.2) and a Transfer -/// Event names the Transfer TRB that generated it (§6.4.2.1, with ED clear — -/// no TRB this driver enqueues sets Event Data). Matching on anything coarser -/// — "the next Command Completion Event", or "the next completion on this -/// endpoint" — hands an operation that ran out its deadline and answered -/// afterwards to whatever asked next. -/// -/// **The transfer arm did the coarser thing until 2026-08-20**, with -/// [`Stages::DataThenStatus`] standing in for the ambiguity a control -/// transfer's two completions on one endpoint create. The two are now one -/// answer: a stage is named by its own TRB, so the second stage is not -/// something the *count* has to keep track of, and an abandoned transfer's late -/// completion matches nothing rather than matching the next asker. -/// -/// `slot` and `dci` stay beside the address because they cost nothing and are a -/// second, independent statement of the same fact: an event whose endpoint -/// disagrees with the TRB the driver put on that endpoint's ring is a -/// controller contradicting itself, and one this driver must not act on. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Await { - Command { trb: u64 }, - Transfer { slot: u8, dci: u8, trb: u64 }, -} - -/// xHCI 1.2 Table 6-90's Success, and its Short Packet — a transfer that ended -/// on a packet smaller than the endpoint's maximum, which is a success with a -/// residue and not an error. Together they are the codes after which the -/// controller still owes whatever stages are left. -pub const CC_SUCCESS: u32 = 1; -pub const CC_SHORT_PACKET: u32 = 13; -/// Table 6-90's Stall Error: the device STALLed the transfer. -pub const CC_STALL: u32 = 6; - -/// What the controller still owes one operation after the completion it was -/// submitted on. -/// -/// **A control transfer with a data stage produces two completions.** The data -/// stage carries IOC so the driver can learn how many bytes actually arrived, -/// and the status stage carries its own — so an operation that ended on the -/// first would leave the second to be matched by whatever asked next on the -/// same endpoint. -/// -/// **A second [`Await`] and not a count**, which is the difference the TRB -/// address makes: the two stages are two TRBs at two addresses, so what is owed -/// after the data stage is a *named* event rather than "one more of something -/// on this endpoint". A count could only be spent by whatever arrived next. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Stages { - /// Nothing further: the operation is over on the completion of the TRB it - /// was submitted with. - One, - /// A control transfer's status stage, on its own TRB, after the data stage - /// the operation was submitted with. - DataThenStatus(Await), -} - -/// How an operation ended. -/// -/// **Two answered variants and not one number named by context.** A Command -/// Completion Event and a Transfer Event carry a second value each and they are -/// different values in different fields — the Slot ID the controller allocated, -/// and the bytes of the buffer it did not move. One field for both would be a -/// number whose meaning only a comment states, and reading a command's residue -/// would compile. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Outcome { - /// A command completed. `slot` is the event's Slot ID field, which is the - /// controller's answer to Enable Slot and an echo of what was asked for - /// every other command. - Command { code: u32, slot: u8 }, - /// A transfer completed. `code` is the last stage's — a failing data stage - /// ends the operation where it is — and `residue` is what the *first* stage - /// said it did not move, because that is the only stage carrying a buffer. - Transfer { code: u32, residue: u32 }, - /// Nothing arrived inside the deadline. Distinct from a failing code - /// because the two say different things about the device: a code is a - /// device that spoke, and this is one that did not. - Silent, -} - -impl Outcome { - /// Whether the controller answered Success, which is what most callers ask - /// and the only thing they do with the code. - pub fn succeeded(self) -> bool { - matches!( - self, - Self::Command { code: CC_SUCCESS, .. } | Self::Transfer { code: CC_SUCCESS, .. } - ) - } - - /// The completion code, and `None` for an operation nothing answered. - pub fn code(self) -> Option { - match self { - Self::Command { code, .. } | Self::Transfer { code, .. } => Some(code), - Self::Silent => None, - } - } -} - -/// Bytes a transfer of `asked` bytes moved, from its event's residue — the -/// bytes it did not move (xHCI 1.2 §6.4.2.1, TRB Transfer Length). A residue -/// past `asked` is the controller contradicting itself, and believing it -/// would name bytes past the buffer: it reads as nothing moved. -pub const fn moved(asked: u32, residue: u32) -> u32 { - asked.saturating_sub(residue) -} - -struct Job { - what: W, - /// What the **next** completion this operation owes must name. A two-stage - /// control transfer moves it on to its status stage when the data stage - /// arrives, so the slot never owes an unnamed event. - on: Await, - /// The stage after [`Self::on`], and `None` once nothing further is owed. - then: Option, - /// Wall clock, and deliberately not a spin count: the pass that submitted - /// this gave itself back, so what notices the deadline is a later pass and - /// not this one. - deadline: Nanos, - answer: Option, - /// The first completion's second number, and `None` until one has arrived. - /// Kept apart from `answer` because for a two-stage transfer the two come - /// from different stages: only the data stage carries a buffer. - param: Option, -} - -/// The slot, holding at most one operation. -/// -/// **One and not a queue.** The command ring is a single queue the controller -/// works in order, the driver this replaces ran every command strictly serially -/// because each was a submit followed by its own wait, and a second slot would -/// buy concurrency the hardware does not offer at the price of the cancellation -/// rule below having a simple form. What a caller sees when the slot is taken -/// is [`Self::busy`], and every caller of that defers rather than queues. -pub struct Outstanding { - job: Option>, -} - -impl Default for Outstanding { - fn default() -> Self { - Self::EMPTY - } -} - -impl Outstanding { - pub const EMPTY: Self = Self { job: None }; - - /// Whether an operation is in flight. A caller that would submit one, free - /// memory an in-flight one still names, or start work that needs the - /// controller's answer first, defers on this. - pub fn busy(&self) -> bool { - self.job.is_some() - } - - /// What the outstanding operation is for, for a caller deciding whether it - /// is the one to cancel. - pub fn what(&self) -> Option<&W> { - self.job.as_ref().map(|j| &j.what) - } - - /// Record an operation the caller has just submitted to the controller. - /// - /// Panics if one is already outstanding: the slot is the whole reason - /// [`Self::busy`] exists, and a caller that submitted without asking has - /// left a completion nothing will ever match. - pub fn submit(&mut self, what: W, on: Await, stages: Stages, deadline: Nanos) { - assert!(self.job.is_none(), "a second operation was submitted over an outstanding one"); - let then = match stages { - Stages::One => None, - Stages::DataThenStatus(status) => Some(status), - }; - self.job = Some(Job { what, on, then, deadline, answer: None, param: None }); - } - - /// Offer an arriving completion to the outstanding operation, and say - /// whether it was the one being waited for. - /// - /// **Recorded, never acted on here.** The drain that produces these runs - /// inside the waits of a caller that is after one particular event, so - /// anything done from this point would consume events that caller owns. - /// - /// `true` also for a stage that is not the last: the completion belongs to - /// this operation whether or not it ends it, and a caller that took it back - /// would hand the data stage of its own transfer to a bound device. - /// - /// `param` is the event's second number — the Slot ID for a command and the - /// residue for a transfer — and which it is comes from `by` rather than - /// from the caller saying, so the two cannot be swapped at a call site. - pub fn answered(&mut self, by: Await, code: u32, param: u32) -> bool { - let Some(job) = self.job.as_mut() else { return false }; - if job.on != by || job.answer.is_some() { - return false; - } - let param = *job.param.get_or_insert(param); - // A stage that did not succeed is the end of the operation whatever is - // still owed: an errored data stage halts the endpoint, so the status - // TRB behind it never runs and waiting for it spends the whole deadline - // learning that. The `take` is what makes that true of the *slot* and - // not only of this call — a failed data stage leaves nothing owed, so - // the status TRB's own event, should the controller produce one anyway, - // matches nothing. - match job.then.take() { - Some(status) if matches!(code, CC_SUCCESS | CC_SHORT_PACKET) => job.on = status, - _ => { - job.answer = Some(match by { - Await::Command { .. } => Outcome::Command { code, slot: param as u8 }, - Await::Transfer { .. } => Outcome::Transfer { code, residue: param }, - }) - } - } - true - } - - /// Take the operation if it is over, with what it was for. - /// - /// An answer wins over the clock: a completion that arrived inside the - /// deadline is the controller's answer whenever the caller gets round to - /// asking, and calling it silent because a pass was late would abandon a - /// device that did everything right. - pub fn finished(&mut self, now: Nanos) -> Option<(W, Outcome)> { - let job = self.job.as_ref()?; - let outcome = match job.answer { - Some(outcome) => outcome, - None if now >= job.deadline => Outcome::Silent, - None => return None, - }; - Some((self.job.take()?.what, outcome)) - } - - /// Forget the outstanding operation, for a caller that has learnt its - /// answer cannot matter — the device it was for has left the bus. The - /// completion may still arrive and is then an event addressed to nobody, - /// which is what every abandoned wait already produced. - pub fn cancel(&mut self) -> Option { - Some(self.job.take()?.what) - } - - /// When the deadline runs out, so a caller with nothing else to do knows - /// when to come back. `None` when nothing is outstanding. - pub fn wake_at(&self) -> Option { - Some(self.job.as_ref()?.deadline) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const CMD: Await = Await::Command { trb: 0x1000 }; - /// A control transfer's two stages on one endpoint: two TRBs, sixteen bytes - /// apart, because that is how [`crate::job`]'s caller enqueues them. - const EP0_DATA: Await = Await::Transfer { slot: 3, dci: 1, trb: 0x2000 }; - const EP0_STATUS: Await = Await::Transfer { slot: 3, dci: 1, trb: 0x2010 }; - /// A bulk transfer, which is one TRB and one completion. - const BULK: Await = Await::Transfer { slot: 3, dci: 4, trb: 0x3000 }; - - fn answered(code: u32) -> Outcome { - Outcome::Command { code, slot: 0 } - } - - fn slot() -> Outstanding<&'static str> { - let mut o = Outstanding::EMPTY; - o.submit("teardown", CMD, Stages::One, 100); - o - } - - /// A control transfer with a data stage, which is what every descriptor read - /// in the driver is. - fn two_stage() -> Outstanding<&'static str> { - let mut o = Outstanding::EMPTY; - o.submit("descriptor", EP0_DATA, Stages::DataThenStatus(EP0_STATUS), 100); - o - } - - #[test] - fn an_empty_slot_is_not_busy_and_has_no_deadline() { - let mut o: Outstanding<&str> = Outstanding::EMPTY; - assert!(!o.busy()); - assert_eq!(o.wake_at(), None); - assert!(o.finished(u64::MAX).is_none()); - assert!(!o.answered(CMD, 1, 0)); - } - - #[test] - fn a_completion_for_another_trb_is_not_this_one() { - let mut o = slot(); - assert!(!o.answered(Await::Command { trb: 0x2000 }, 1, 0)); - assert!(!o.answered(BULK, 1, 0)); - assert!(o.finished(99).is_none()); - assert!(o.answered(CMD, 1, 0)); - } - - #[test] - fn a_transfer_matches_on_its_trb_and_on_both_halves_of_its_endpoint() { - let mut o = Outstanding::EMPTY; - o.submit((), BULK, Stages::One, 100); - assert!(!o.answered(Await::Transfer { slot: 3, dci: 5, trb: 0x3000 }, 1, 0)); - assert!(!o.answered(Await::Transfer { slot: 4, dci: 4, trb: 0x3000 }, 1, 0)); - assert!(!o.answered(Await::Transfer { slot: 3, dci: 4, trb: 0x3010 }, 1, 0)); - assert!(o.answered(BULK, 1, 0)); - } - - /// **The defect the TRB address closes.** A transfer the driver stopped - /// waiting for is still the device's to answer, and the answer arrives on - /// the same endpoint as whatever asked next. Matching on the endpoint hands - /// the first transfer's completion — and its residue — to the second. - #[test] - fn a_late_completion_from_an_abandoned_transfer_answers_nobody() { - let mut o = Outstanding::EMPTY; - o.submit("abandoned", BULK, Stages::One, 100); - assert_eq!(o.cancel(), Some("abandoned")); - let next = Await::Transfer { slot: 3, dci: 4, trb: 0x3010 }; - o.submit("the next command", next, Stages::One, 200); - // The abandoned transfer's completion, on the same slot and the same - // endpoint, carrying a residue that is not this transfer's. - assert!(!o.answered(BULK, CC_SUCCESS, 4096)); - assert!(o.finished(150).is_none()); - assert!(o.answered(next, CC_SUCCESS, 0)); - assert_eq!( - o.finished(150), - Some(("the next command", Outcome::Transfer { code: CC_SUCCESS, residue: 0 })) - ); - } - - #[test] - fn the_deadline_produces_silence_and_nothing_else() { - let mut o = slot(); - assert!(o.finished(99).is_none()); - assert_eq!(o.finished(100), Some(("teardown", Outcome::Silent))); - assert!(!o.busy()); - } - - /// The case a pass-based driver hits routinely: the completion landed on - /// time and the pass that looks is late. Calling that silent would abandon - /// a device that answered. - #[test] - fn an_answer_outlives_the_deadline() { - let mut o = slot(); - assert!(o.answered(CMD, 6, 0)); - assert_eq!(o.finished(u64::MAX), Some(("teardown", answered(6)))); - } - - #[test] - fn only_the_first_answer_counts() { - let mut o = slot(); - assert!(o.answered(CMD, 1, 0)); - assert!(!o.answered(CMD, 6, 0)); - assert_eq!(o.finished(0), Some(("teardown", answered(1)))); - } - - #[test] - fn a_cancelled_operation_leaves_the_slot_free() { - let mut o = slot(); - assert_eq!(o.what(), Some(&"teardown")); - assert_eq!(o.cancel(), Some("teardown")); - assert!(!o.busy()); - assert!(!o.answered(CMD, 1, 0)); - } - - #[test] - #[should_panic(expected = "outstanding")] - fn submitting_over_an_outstanding_operation_panics() { - let mut o = slot(); - o.submit("recovery", Await::Command { trb: 0x2000 }, Stages::One, 200); - } - - /// The defect the second stage exists for: both completions land in one - /// drain, and an operation that ended on the data stage would leave the - /// status stage to answer whatever asked next on the same endpoint. - #[test] - fn a_data_stage_does_not_end_a_transfer_that_still_owes_its_status() { - let mut o = two_stage(); - assert!(o.answered(EP0_DATA, CC_SUCCESS, 4)); - assert!(o.finished(0).is_none()); - assert!(o.busy()); - assert!(o.answered(EP0_STATUS, CC_SUCCESS, 0)); - assert_eq!( - o.finished(0), - Some(("descriptor", Outcome::Transfer { code: CC_SUCCESS, residue: 4 })) - ); - } - - /// The stages are named and therefore ordered: a status stage cannot answer - /// the data stage the operation is still waiting for, and a data stage - /// cannot be counted twice. - #[test] - fn a_stage_is_answered_by_its_own_trb_and_in_its_own_order() { - let mut o = two_stage(); - assert!(!o.answered(EP0_STATUS, CC_SUCCESS, 0)); - assert!(o.answered(EP0_DATA, CC_SUCCESS, 4)); - assert!(!o.answered(EP0_DATA, CC_SUCCESS, 4)); - assert!(o.answered(EP0_STATUS, CC_SUCCESS, 0)); - } - - /// Only the data stage carries a buffer, so its residue is the one that - /// says how many bytes arrived — and the status stage's is meaningless. - #[test] - fn the_residue_is_the_first_stages_and_the_code_is_the_last() { - let mut o = two_stage(); - assert!(o.answered(EP0_DATA, CC_SHORT_PACKET, 10)); - assert!(o.answered(EP0_STATUS, CC_SUCCESS, 999)); - assert_eq!( - o.finished(0), - Some(("descriptor", Outcome::Transfer { code: CC_SUCCESS, residue: 10 })) - ); - } - - /// A failing data stage halts the endpoint, so the status TRB behind it - /// never runs. Waiting for it spends the whole deadline learning that — - /// and a status event the controller produces anyway belongs to nobody. - #[test] - fn a_failing_stage_ends_the_transfer_whatever_is_still_owed() { - let mut o = two_stage(); - assert!(o.answered(EP0_DATA, 6, 8)); - assert!(!o.answered(EP0_STATUS, CC_SUCCESS, 0)); - assert_eq!( - o.finished(0), - Some(("descriptor", Outcome::Transfer { code: 6, residue: 8 })) - ); - } - - /// A status stage that fails after a data stage that did not is the - /// device's verdict, and it is the code the caller sees. - #[test] - fn a_failing_status_stage_is_the_answer() { - let mut o = two_stage(); - assert!(o.answered(EP0_DATA, CC_SUCCESS, 0)); - assert!(o.answered(EP0_STATUS, 4, 0)); - assert_eq!( - o.finished(0), - Some(("descriptor", Outcome::Transfer { code: 4, residue: 0 })) - ); - } - - #[test] - fn a_transfer_that_answers_one_stage_and_stops_is_silent() { - let mut o = two_stage(); - assert!(o.answered(EP0_DATA, CC_SUCCESS, 0)); - assert_eq!(o.finished(100), Some(("descriptor", Outcome::Silent))); - } - - /// A short packet moved what it did not leave; a whole transfer left - /// nothing; a residue past the request names no byte past the buffer. - #[test] - fn a_transfer_moved_what_its_residue_did_not_leave() { - assert_eq!(moved(8, 5), 3, "a short packet"); - assert_eq!(moved(8, 0), 8, "a whole transfer"); - assert_eq!(moved(8, 8), 0, "nothing moved"); - assert_eq!(moved(8, 9), 0, "a residue past the request"); - assert_eq!(moved(18, 0x00FF_FFFF), 0, "the field's largest residue"); - } -} diff --git a/lib/xhci/src/ladder.rs b/lib/xhci/src/ladder.rs deleted file mode 100644 index bf63967e749..00000000000 --- a/lib/xhci/src/ladder.rs +++ /dev/null @@ -1,482 +0,0 @@ -//! What a mass-storage device's transport is owed after a break: a ladder of -//! three rungs, each ended by the device's own answer and by nothing else. -//! -//! 1. [`Rung::ClassReset`] — Bulk-Only Transport 1.0 §5.3.4's Reset Recovery -//! ([`crate::reset_recovery`]), which the class defines and a device may -//! answer without honouring: §3.1 has it "ready … for the next CBW" once it -//! answers the reset, and the requests give a host no way to tell a device -//! that is from one that is still inside the command it was given. -//! 2. [`Rung::PortReset`] — the reset no USB device may ignore (USB 2.0 §9.1.1, -//! Figure 9-1: every state returns to Default on it), and the enumeration -//! that follows a reset ([`PORT_RESET`]), on the slot and under the disk -//! number the device already has, so the volume on it carries on. -//! 3. [`Rung::Offline`] — the device did not answer after a port reset. Its -//! endpoints are stopped and **the last thing it is sent is a reset**, so -//! whatever command it was inside is over and the next host finds a device -//! in its Default state. -//! -//! **Verified means a TEST UNIT READY answered under its own tag.** It is the -//! one command with no data phase, so it asks the device to move nothing. -//! -//! **A device owed Data-Out bytes is not given the class reset** ([`enters_at`]). -//! The rung's verification is itself 31 bytes on the Bulk-Out, and a device -//! that answered the class reset without leaving its data phase takes them as -//! the data it is owed: the question cannot be asked there without being part -//! of the answer. The port reset's can — after a reset and a -//! SET_CONFIGURATION no data phase is open (USB 2.0 §9.1.1, §9.4.7) — so that -//! is where such a break enters, and nothing reaches the Bulk-Out before it. -//! -//! **A rung is climbed once per run of breaks.** A rung that verified and whose -//! command then broke again has not brought the transport back, and asking it -//! again asks the same question of the same device: the next break climbs on. -//! Only a completed round trip ends the run. - -/// One rung of the ladder. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] -pub enum Rung { - ClassReset, - PortReset, - Offline, -} - -impl Rung { - /// What a line about this rung calls it. - pub fn named(self) -> &'static str { - match self { - Self::ClassReset => "class reset", - Self::PortReset => "port reset", - Self::Offline => "last reset", - } - } -} - -/// The most breaks a run can hold before its device is offline: one per rung. -pub const MOST_BREAKS: u8 = 3; - -/// Where in its round trip a break left the device. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Left { - /// Inside a Data-Out phase: it holds a command block and is owed bytes the - /// host has stopped sending. - OwedDataOut, - /// Anywhere else: no command block taken, a Data-In it is sending, or a - /// status it is holding. - Elsewhere, -} - -/// Where a break in `phase` of a command left the device; `data_out` is whether -/// the command has a Data-Out phase. -/// -/// **A command-phase break of such a command is a device owed data too**: a CBW -/// whose handshake was lost after the device took it is a command the device -/// holds, and the bytes it then waits for are the Data-Out phase's. Only a break -/// in the status phase is past the data. -pub fn left(phase: crate::bot::Phase, data_out: bool) -> Left { - use crate::bot::Phase; - match phase { - Phase::Command | Phase::DataOwed | Phase::Data if data_out => Left::OwedDataOut, - _ => Left::Elsewhere, - } -} - -/// The lowest rung a break that left the device there may be answered with. -pub fn enters_at(left: Left) -> Rung { - match left { - Left::OwedDataOut => Rung::PortReset, - Left::Elsewhere => Rung::ClassReset, - } -} - -/// The rung a break climbs to: above every rung this run of breaks has climbed -/// (`climbed` is the highest), and never below where the break itself enters. -/// A rung that did not verify is the next break, [`Left::Elsewhere`]. -pub fn next(climbed: Option, left: Left) -> Rung { - let above = match climbed { - None => Rung::ClassReset, - Some(Rung::ClassReset) => Rung::PortReset, - Some(Rung::PortReset | Rung::Offline) => Rung::Offline, - }; - above.max(enters_at(left)) -} - -/// A device's run of breaks: how many, and the highest rung among them. -/// -/// **Per device, not per command**: the run it bounds is the device's, however -/// many callers and operations it is spread over, and only a completed round -/// trip ends it ([`Self::over`]). -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Run { - breaks: u8, - climbed: Option, -} - -/// What one break costs the run. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Climb { - pub rung: Rung, - /// The run's first rung is above the class reset: the device is owed the - /// Data-Out of the command that broke ([`enters_at`]). - pub skips_class_reset: bool, -} - -impl Run { - pub const NONE: Self = Self { breaks: 0, climbed: None }; - - /// Breaks in the run so far. - pub fn breaks(self) -> u8 { - self.breaks - } - - /// A break that left the device `left`: counted, and the rung it climbs. - pub fn broke(&mut self, left: Left) -> Climb { - self.breaks = self.breaks.saturating_add(1); - let rung = next(self.climbed, left); - let skips_class_reset = self.climbed.is_none() && rung != Rung::ClassReset; - self.climbed = Some(rung); - Climb { rung, skips_class_reset } - } - - /// A round trip completed: the run is over. How many breaks it held. - pub fn over(&mut self) -> u8 { - core::mem::replace(self, Self::NONE).breaks - } - - /// The rung this run last climbed ended `ended`, below [`Rung::Offline`], - /// on a port that `holds` its device or not (`Portsc::holds`, read once - /// the rung has ended). - /// - /// **A device its port no longer holds left, whichever rung it was and - /// however the rung ended.** That is no break, and no rung above reaches - /// the device: its port's teardown owns what it held. Otherwise the rung - /// is the next break. - pub fn unverified(&mut self, ended: &Unverified, holds: bool) -> AfterRung { - if !holds { - return AfterRung::Left; - } - // After a step that was not answered the event is spent: the rung has - // commanded the pair since, and only the fields speak for it now. - let broke = match ended { - Unverified::OutOfStep(why) => why.event(), - Unverified::Failed => None, - }; - // A rung's own TEST UNIT READY has no data phase to be left in. - AfterRung::Climbs { climb: self.broke(Left::Elsewhere), broke } - } -} - -/// How a rung below [`Rung::Offline`] ended without bringing its device back -/// in step; `W` is the driver's reason for a silence. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Unverified { - /// Every step before its TEST UNIT READY was answered, and that broke this - /// way: a break like the one recovered from. - OutOfStep(crate::bot::Broke), - /// A step before its TEST UNIT READY was not answered; the device was - /// asked nothing after it. - Failed, -} - -/// What a climb does after a rung that did not verify ([`Run::unverified`]). -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum AfterRung { - /// The device left. - Left, - /// The next break: the rung it climbs, and the transfer event that rung - /// quiesces against, where one ended the round trip. - Climbs { climb: Climb, broke: Option<(crate::reset_recovery::Pipe, u32)> }, -} - -/// One step of [`Rung::PortReset`], in the order taken. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum PortStep { - /// Both bulk endpoints off their transfers (xHCI 1.2 §4.6.11: "software - /// should stop all endpoint activity before issuing a Reset Device - /// Command"). A pair that cannot be stopped does not end the rung: the - /// Reset Device below terminates its activity and disables it. - Quiesce, - /// The most reset the port has ([`crate::port::offline_reset`]), waited - /// for, its change flags consumed. - Reset, - /// The reset recovery time ([`RESET_RECOVERY_NS`]): the device owes no - /// answer yet. - Settle, - /// Reset Device (§4.6.11): the slot to Default and address 0, every - /// endpoint but the control endpoint Disabled — what the device now is. - ResetDevice, - /// Address Device with BSR clear, which §4.6.5 defines from the Default - /// state: SET_ADDRESS, the first request the reset device answers. - Address, - /// SET_CONFIGURATION with the value the bind chose. - Configure, - /// Configure Endpoint adding the bulk pair on fresh rings, described as the - /// bind described it. - AddEndpoints, -} - -/// [`Rung::PortReset`], up to the TEST UNIT READY it ends on. A step that fails -/// ends the rung unverified, [`PortStep::Quiesce`] alone excepted. -pub const PORT_RESET: [PortStep; 7] = [ - PortStep::Quiesce, - PortStep::Reset, - PortStep::Settle, - PortStep::ResetDevice, - PortStep::Address, - PortStep::Configure, - PortStep::AddEndpoints, -]; - -/// USB 2.0 §7.1.7.5 has a device answer 10 ms after a reset (TRSTRCY). No -/// document names the 50 ms this waits: -/// `issues/values-no-document-states-stand-where-another-os-was-their-source.md`. -pub const RESET_RECOVERY_NS: u64 = 50_000_000; - -/// Whether a failed `step` ends [`Rung::PortReset`]. -pub fn ends_the_rung(step: PortStep) -> bool { - step != PortStep::Quiesce -} - -/// What a port reads after the rung's reset, against what the device was -/// enumerated as. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum AfterReset { - /// The same device at the same speed, its port enabled: it is addressed. - Enumerate, - /// The reset was written and its completion was not seen. - NeverFinished, - /// Nothing is connected: the device left, and its port's teardown owns - /// what it held. - Left, - /// The reset completed and the port is not enabled (§4.19.5's failure). - NotEnabled, - /// It trained at another speed, so the endpoint descriptors the bind read - /// no longer describe it. - SpeedChanged { was: u8, now: u8 }, -} - -impl AfterReset { - /// Whether the device was reset: the last thing it was sent is one. - pub fn finished(self) -> bool { - !matches!(self, Self::NeverFinished | Self::Left) - } - - /// Whether the port rung goes on past its reset: only for the device it - /// was enumerated as, on its port. Any other reading ends the rung, and - /// nothing is sent to a port that reads empty. - pub fn goes_on(self) -> bool { - self == Self::Enumerate - } -} - -impl core::fmt::Display for AfterReset { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Enumerate => f.write_str("reset, and the port is enabled"), - Self::NeverFinished => f.write_str("the reset's completion was never seen"), - Self::Left => f.write_str("nothing is connected, so its port's teardown takes it from here"), - Self::NotEnabled => f.write_str("reset, and the port is not enabled"), - Self::SpeedChanged { was, now } => { - write!(f, "reset, and it trained at speed {now}, not the {was} it was enumerated at") - } - } - } -} - -/// `finished` is whether the reset's completion was seen; `connected`, -/// `enabled` and `speed` are the port's after it; `was` is the speed the device -/// was enumerated at. -pub fn after_reset(finished: bool, connected: bool, enabled: bool, speed: u8, was: u8) -> AfterReset { - if !connected { - AfterReset::Left - } else if !finished { - AfterReset::NeverFinished - } else if !enabled { - AfterReset::NotEnabled - } else if speed != was { - AfterReset::SpeedChanged { was, now: speed } - } else { - AfterReset::Enumerate - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const LEFT: [Left; 2] = [Left::OwedDataOut, Left::Elsewhere]; - - /// Every run, however its breaks left the device: no rung is climbed - /// twice, none below where a break enters, the run is offline within - /// [`MOST_BREAKS`], and offline is never reached before a port reset. - #[test] - fn every_run_of_breaks_climbs_strictly_and_ends_offline_after_a_port_reset() { - for first in LEFT { - for second in LEFT { - for third in LEFT { - let mut climbed = None; - let mut rungs = [None; MOST_BREAKS as usize]; - for (at, left) in [first, second, third].into_iter().enumerate() { - let rung = next(climbed, left); - assert!(climbed.is_none_or(|last| rung > last), "{climbed:?} then {rung:?}"); - assert!(rung >= enters_at(left)); - rungs[at] = Some(rung); - climbed = Some(rung); - if rung == Rung::Offline { - break; - } - } - assert_eq!(climbed, Some(Rung::Offline), "{first:?} {second:?} {third:?}"); - let offline = rungs.iter().position(|r| *r == Some(Rung::Offline)).expect("reached"); - assert_eq!(rungs[offline - 1], Some(Rung::PortReset), "{rungs:?}"); - } - } - } - } - - /// The run is the device's: every break climbs above the last, the count - /// reaches [`MOST_BREAKS`] exactly where the run is offline, and a completed - /// round trip starts the next run at the bottom. - #[test] - fn a_run_climbs_one_rung_per_break_until_a_round_trip_ends_it() { - let mut run = Run::NONE; - assert_eq!(run.broke(Left::Elsewhere), Climb { rung: Rung::ClassReset, skips_class_reset: false }); - assert_eq!(run.broke(Left::Elsewhere), Climb { rung: Rung::PortReset, skips_class_reset: false }); - assert_eq!(run.broke(Left::Elsewhere).rung, Rung::Offline); - assert_eq!(run.breaks(), MOST_BREAKS); - assert_eq!(run.over(), MOST_BREAKS); - assert_eq!(run, Run::NONE); - assert_eq!(run.broke(Left::Elsewhere).rung, Rung::ClassReset, "a run begun again climbs from the bottom"); - } - - /// Said once, at the first rung of a run, and only when that rung is the - /// port reset. - #[test] - fn only_a_run_that_begins_owed_data_out_skips_the_class_reset() { - let mut run = Run::NONE; - assert_eq!(run.broke(Left::OwedDataOut), Climb { rung: Rung::PortReset, skips_class_reset: true }); - assert_eq!(run.broke(Left::OwedDataOut), Climb { rung: Rung::Offline, skips_class_reset: false }); - let mut run = Run::NONE; - run.broke(Left::Elsewhere); - assert!(!run.broke(Left::OwedDataOut).skips_class_reset, "the class reset was climbed"); - } - - /// Every run that reaches a rung below [`Rung::Offline`], each way that - /// rung can end unverified: its device left exactly where its port no - /// longer holds it, which is no break; otherwise the rung is the next - /// break, and the next rung quiesces against the event that ended the - /// TEST UNIT READY, where one did. - #[test] - fn a_rung_that_did_not_verify_left_exactly_where_its_port_no_longer_holds_the_device() { - use crate::bot::{Broke, Phase}; - use crate::reset_recovery::Pipe; - let stalled = Broke::<()>::Code { phase: Phase::Command, code: 6, pipe: Pipe::Out }; - let endings = [ - (Unverified::Failed, None), - (Unverified::OutOfStep(stalled), Some((Pipe::Out, 6))), - (Unverified::OutOfStep(Broke::Gone { phase: Phase::Command }), None), - (Unverified::OutOfStep(Broke::Silence { phase: Phase::Status, why: () }), None), - ]; - let runs: [(&[Left], Rung); 3] = [ - (&[Left::Elsewhere], Rung::ClassReset), - (&[Left::Elsewhere, Left::Elsewhere], Rung::PortReset), - (&[Left::OwedDataOut], Rung::PortReset), - ]; - for (breaks, rung) in runs { - let mut at = Run::NONE; - for left in breaks { - at.broke(*left); - } - assert_eq!(at.climbed, Some(rung), "{breaks:?}"); - for (ended, event) in endings { - let mut run = at; - assert_eq!(run.unverified(&ended, false), AfterRung::Left, "{rung:?} {ended:?}"); - assert_eq!(run, at, "a device that left is no break: {rung:?} {ended:?}"); - let mut next = at; - let climb = next.broke(Left::Elsewhere); - assert_eq!( - run.unverified(&ended, true), - AfterRung::Climbs { climb, broke: event }, - "{rung:?} {ended:?}" - ); - assert_eq!(run, next, "{rung:?} {ended:?}"); - } - } - } - - #[test] - fn a_break_elsewhere_climbs_every_rung() { - assert_eq!(next(None, Left::Elsewhere), Rung::ClassReset); - assert_eq!(next(Some(Rung::ClassReset), Left::Elsewhere), Rung::PortReset); - assert_eq!(next(Some(Rung::PortReset), Left::Elsewhere), Rung::Offline); - } - - #[test] - fn a_device_owed_data_out_is_never_given_the_class_reset() { - for climbed in [None, Some(Rung::ClassReset), Some(Rung::PortReset)] { - assert_ne!(next(climbed, Left::OwedDataOut), Rung::ClassReset, "{climbed:?}"); - } - assert_eq!(next(None, Left::OwedDataOut), Rung::PortReset); - } - - /// A command that sends data leaves its device owed it from the moment its - /// CBW may have been taken until its data phase is done; nothing else does. - #[test] - fn a_break_before_a_data_out_phase_is_done_leaves_the_device_owed_it() { - use crate::bot::Phase; - for phase in [Phase::Command, Phase::DataOwed, Phase::Data] { - assert_eq!(left(phase, true), Left::OwedDataOut, "{phase:?}"); - assert_eq!(left(phase, false), Left::Elsewhere, "{phase:?} of a command sending nothing"); - } - for phase in [Phase::Closed, Phase::StatusOwed, Phase::Status] { - for data_out in [true, false] { - assert_eq!(left(phase, data_out), Left::Elsewhere, "{phase:?}"); - } - } - } - - /// The order is the specifications': nothing reaches the device between - /// the reset and its address, and nothing is configured before it is - /// addressed. - #[test] - fn the_port_rung_resets_then_addresses_then_configures_then_asks() { - let at = |step| PORT_RESET.iter().position(|s| *s == step).expect("a step of the rung"); - assert_eq!(at(PortStep::Quiesce), 0); - assert!(at(PortStep::Reset) < at(PortStep::Settle)); - assert!(at(PortStep::Settle) < at(PortStep::ResetDevice)); - assert!(at(PortStep::ResetDevice) < at(PortStep::Address)); - assert!(at(PortStep::Address) < at(PortStep::Configure)); - assert!(at(PortStep::Configure) < at(PortStep::AddEndpoints)); - assert_eq!(at(PortStep::AddEndpoints), PORT_RESET.len() - 1); - } - - #[test] - fn only_a_quiesce_that_failed_lets_the_rung_go_on() { - for step in PORT_RESET { - assert_eq!(ends_the_rung(step), step != PortStep::Quiesce, "{step:?}"); - } - } - - #[test] - fn a_device_is_addressed_again_only_as_what_it_was_enumerated_as() { - assert_eq!(after_reset(true, true, true, 3, 3), AfterReset::Enumerate); - assert_eq!(after_reset(true, false, false, 0, 3), AfterReset::Left); - assert_eq!(after_reset(true, false, true, 3, 3), AfterReset::Left, "connect outranks enable"); - assert_eq!(after_reset(false, true, true, 3, 3), AfterReset::NeverFinished); - assert_eq!(after_reset(true, true, false, 3, 3), AfterReset::NotEnabled); - assert_eq!(after_reset(true, true, true, 4, 3), AfterReset::SpeedChanged { was: 3, now: 4 }); - for left in [AfterReset::NeverFinished, AfterReset::Left] { - assert!(!left.finished(), "{left:?}"); - } - for left in [AfterReset::Enumerate, AfterReset::NotEnabled, AfterReset::SpeedChanged { was: 3, now: 4 }] { - assert!(left.finished(), "{left:?}"); - } - assert!(AfterReset::Enumerate.goes_on()); - for after in [ - AfterReset::NeverFinished, - AfterReset::Left, - AfterReset::NotEnabled, - AfterReset::SpeedChanged { was: 3, now: 4 }, - ] { - assert!(!after.goes_on(), "{after:?}"); - } - } -} diff --git a/lib/xhci/src/late.rs b/lib/xhci/src/late.rs deleted file mode 100644 index 6757c7ce10b..00000000000 --- a/lib/xhci/src/late.rs +++ /dev/null @@ -1,120 +0,0 @@ -//! When a wait on the event ring gives up. -//! -//! **A deadline is the controller's silence, so past it the ring is still read, -//! and the wait gives up at the first empty read.** A CPU can be held past a -//! deadline with the answer already posted: QEMU answers a doorbell inside the -//! vCPU's write to it, and a Stop Endpoint cancelling a USB disk's in-flight -//! SCSI request there waits out the host's flush first. -//! -//! **Checked before every read, not only an empty one**, and a ring read past -//! the deadline at most once around: a ring that keeps producing events the -//! wait is not waiting for would otherwise make the bound unreachable, and the -//! caller holds the controller lock and a block operation with `IF` clear for -//! the whole of it. - -/// Nanoseconds since boot. -pub type Nanos = u64; - -/// One wait's deadline, and how many reads it has taken past it. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Late { - deadline: Nanos, - /// The most reads a wait takes once its deadline has passed: one ring's - /// worth, so every event already posted when the deadline passed is read. - ring: usize, - read_past: usize, -} - -impl Late { - /// A wait that ends at `deadline` on a ring of `ring` TRBs. - pub const fn new(deadline: Nanos, ring: usize) -> Self { - Self { deadline, ring, read_past: 0 } - } - - /// Whether the deadline has passed at `now`. - pub fn past(&self, now: Nanos) -> bool { - now >= self.deadline - } - - /// Whether the wait, about to read the ring at `now`, gives up instead; - /// counts the read it allows. - pub fn gives_up(&mut self, now: Nanos) -> bool { - if !self.past(now) { - return false; - } - self.read_past += 1; - self.read_past > self.ring - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const RING: usize = 256; - - /// What a wait does with a ring that answers `event(n)` on its `n`th read, - /// the clock standing at `now(n)`: the reads it took, and whether it ended - /// on the event it waited for. - fn wait( - deadline: Nanos, - now: impl Fn(usize) -> Nanos, - event: impl Fn(usize) -> Option, - ) -> (usize, bool) { - // Far past anything a wait may take, so a wait that never gives up is - // a failure here and not a test that never ends. - const NEVER: usize = 1_000 * RING; - let mut late = Late::new(deadline, RING); - for n in 0..NEVER { - if late.gives_up(now(n)) { - return (n, false); - } - match event(n) { - Some(true) => return (n + 1, true), - Some(false) => continue, - None if late.past(now(n)) => return (n + 1, false), - None => continue, - } - } - panic!("the wait read {NEVER} events and never gave up") - } - - /// The bound this exists to keep: a ring that never stops producing events - /// nobody is waiting for is still given up on, one ring's worth of reads - /// past the deadline. - #[test] - fn a_ring_that_keeps_producing_is_given_up_on_one_ring_past_the_deadline() { - let (reads, answered) = wait(100, |n| 100 + n as Nanos, |_| Some(false)); - assert!(!answered); - assert_eq!(reads, RING, "one ring's worth of reads past the deadline, and no more"); - } - - /// The reason the ring is read past the deadline at all: an answer posted - /// while the CPU was held is taken, not called silence. - #[test] - fn an_answer_posted_before_the_deadline_passed_is_taken_after_it() { - // Held past the deadline with 40 unrelated events and then the answer - // already on the ring. - let (reads, answered) = wait(100, |_| 5_000, |n| Some(n == 40)); - assert!(answered); - assert_eq!(reads, 41); - } - - /// Past the deadline an empty ring is silence at once. - #[test] - fn past_the_deadline_the_first_empty_read_ends_the_wait() { - let (reads, answered) = wait(100, |_| 100, |n| if n < 3 { Some(false) } else { None }); - assert!(!answered); - assert_eq!(reads, 4); - } - - /// Before the deadline nothing is given up, however busy the ring is. - #[test] - fn before_the_deadline_nothing_is_given_up() { - let mut late = Late::new(1_000, RING); - for now in 0..1_000 { - assert!(!late.gives_up(now), "{now}"); - } - assert!(late.past(1_000)); - } -} diff --git a/lib/xhci/src/lib.rs b/lib/xhci/src/lib.rs deleted file mode 100644 index bc2edad02f2..00000000000 --- a/lib/xhci/src/lib.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! The xHCI root-hub port machine, as a decision separated from its effects. -//! -//! The kernel reads PORTSC, asks [`port::PortState::step`] what to do, and does -//! it; nothing here touches a register, a ring or a slot. That split is what -//! lets a host simulator explore the port state space, which is where the -//! laptop's SuperSpeed wedge lives — a state QEMU cannot produce. - -#![cfg_attr(not(feature = "std"), no_std)] -#![forbid(unsafe_code)] - -pub mod bot; -pub mod call; -pub mod configure; -pub mod descriptor; -pub mod enumerate; -pub mod flush; -pub mod identity; -pub mod invariants; -pub mod job; -pub mod late; -pub mod ladder; -pub mod port; -pub mod protocol; -pub mod portsc; -pub mod recovery; -pub mod reset_recovery; -pub mod ring; -pub mod scan; -pub mod xecp; - -pub use job::{Await, Outcome, Outstanding}; -pub use port::{Effect, Gone, Nanos, PortState, Step}; -pub use portsc::{LinkState, Portsc}; -pub use protocol::{Protocol, Protocols}; -pub use recovery::{EndpointState, Recovery}; -pub use ring::{NotOnTheRing, Ring}; diff --git a/lib/xhci/src/reset_recovery.rs b/lib/xhci/src/reset_recovery.rs deleted file mode 100644 index d189599be32..00000000000 --- a/lib/xhci/src/reset_recovery.rs +++ /dev/null @@ -1,626 +0,0 @@ -//! Bulk-Only Transport's Reset Recovery, as the commands an xHC owes before -//! the three requests the class defines. -//! -//! USB Mass Storage Class Bulk-Only Transport 1.0 §5.3.4 makes the recovery -//! three requests, in order: a Bulk-Only Mass Storage Reset (§3.1), a -//! ClearFeature(ENDPOINT_HALT) to the Bulk-In endpoint, and one to the -//! Bulk-Out. **The second and third are not conditional on a halt.** A -//! ClearFeature(ENDPOINT_HALT) reinitialises the device's data toggle whether -//! the endpoint was halted or not (USB 2.0 §9.4.5), so after it the device -//! expects the host to start both pipes at zero. -//! -//! What the host owes for that is two things per endpoint. First, taking it -//! off whatever it was running, to Stopped: Reset Endpoint from Halted (xHCI -//! 1.2 §4.6.8), Stop Endpoint from Running (§4.6.9), Set TR Dequeue Pointer -//! from Error (§4.6.10), nothing from Stopped. Second, zeroing its own toggle -//! or sequence number to match the device's, which for an endpoint that is not -//! Halted a Configure Endpoint with the Drop and Add flags set does (§4.6.8's -//! note, which defines it from Stopped): one such command re-creates both -//! endpoints Running on fresh rings ([`crate::configure`]). -//! -//! **An event outranks the Endpoint State field.** The field is the -//! controller's and may lag the endpoint: §4.8.3's note has its update -//! deferred past an error condition and past a command's completion, and tells -//! software to keep its own image, driven by events. [`Quiescing`] is that -//! image for the length of one quiesce: a transfer event's completion code is -//! where a pipe starts ([`event_state`]), a command that succeeded leaves it -//! Stopped, and the field is read only for a pipe no event has spoken for. -//! -//! **One command per look, and a Context State Error is an answer.** A -//! transfer the driver stopped waiting for can still error, which moves the -//! endpoint out of Running between the look and the Stop Endpoint chosen from -//! it (§4.6.9's note). The state the command was chosen from is then ruled -//! out for that pipe: a field that moved is believed, and one that still reads -//! the ruled-out state is a field that lags, so the plan goes on to where -//! §4.6.9's note says a Running endpoint goes by itself — Halted, then Error. -//! At most [`MOST_LOOKS`] looks. -//! -//! **Every command before the first request.** A request reaches the device -//! and a command does not, so the commands, which end every transfer on the -//! host side, all come first. -//! -//! **The device's own answer ends it, and nothing else does.** §3.1 has the -//! device ready for the next CBW once it answers the reset, and a device that -//! answers and is not is one no request can tell from one that is. So the -//! recovery closes with a TEST UNIT READY and has taken only on a status -//! carrying that command's tag; anything else is the next rung's -//! ([`crate::ladder`]). A command with a buffer is not sent to a device that -//! has not answered one without. - -use crate::recovery::{Command, EndpointState}; - -/// One of a device's two bulk endpoints. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Pipe { - In, - Out, -} - -impl Pipe { - /// The pipe a transfer in this direction runs on. - pub fn of(device_to_host: bool) -> Self { - if device_to_host { - Self::In - } else { - Self::Out - } - } - - fn index(self) -> usize { - match self { - Self::In => 0, - Self::Out => 1, - } - } -} - -/// Completion code 19 (xHCI 1.2 Table 6-90). -pub const CONTEXT_STATE_ERROR: u32 = 19; -const SUCCESS: u32 = 1; - -/// The endpoint state a transfer event's completion code reports, for the -/// codes that report one: §4.8.3 lists Babble Detected (3), USB Transaction -/// Error (4), Stall Error (6) and Split Transaction Error (36) as Halt -/// conditions, and has a TRB Error (5) leave the endpoint in Error. -pub fn event_state(code: u32) -> Option { - match code { - 3 | 4 | 6 | 36 => Some(EndpointState::Halted), - 5 => Some(EndpointState::Error), - _ => None, - } -} - -/// What one look at the pair asks of the driver. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Look { - /// Issue this against this pipe and report its answer to - /// [`Quiescing::answered`], then look again. - Command(Command, Pipe), - /// Both endpoints are Stopped: [`AFTER_QUIESCE`] is what is left. - Stopped, - /// The pair cannot be taken to Stopped, and no further command is owed. - GaveUp(GaveUp), -} - -/// Why a quiesce ended short of Stopped. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum GaveUp { - /// The field reads a state no command leaves; only a Configure Endpoint - /// makes this endpoint again. - NeedsConfigure(Pipe, EndpointState), - /// The controller refused the command with a code that is not about the - /// endpoint's state. - Refused(Command, u32), - /// The controller did not answer the command. - Silent(Command), - /// Every state a command is defined for has been refused for this pipe. - Contradicted(Pipe), - /// [`MOST_LOOKS`] were taken. - OutOfLooks, -} - -/// What an answer meant, for the driver's log; the plan has already acted on -/// it. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Answered { - Took, - /// A Context State Error: the pipe was not in `from`, the state the - /// command was chosen for. - Moved { from: EndpointState }, - /// The next look gives up. - Ended, -} - -/// Looks a quiesce gets. Each pipe costs at most three commands — Stop -/// Endpoint from the Running its field reads, then Reset Endpoint and Set TR -/// Dequeue Pointer for the two states an error can have moved it to under a -/// field that never says so — and the seventh look finds both Stopped. -pub const MOST_LOOKS: u8 = 7; - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -struct Image { - /// What events have said, which outranks the field. - known: Option, - /// States a Context State Error has said the pipe is not in. - not: [bool; 3], -} - -impl Image { - const BLANK: Self = Self { known: None, not: [false; 3] }; - - fn slot(state: EndpointState) -> Option { - match state { - EndpointState::Running => Some(0), - EndpointState::Halted => Some(1), - EndpointState::Error => Some(2), - _ => None, - } - } - - fn ruled_out(&self, state: EndpointState) -> bool { - Self::slot(state).is_some_and(|at| self.not[at]) - } - - /// The state to choose a command from, given what the field reads. - fn believed(&self, field: EndpointState) -> Option { - if matches!(field, EndpointState::Disabled | EndpointState::Unusable(_)) { - return Some(field); - } - if let Some(known) = self.known { - return Some(known); - } - [field, EndpointState::Halted, EndpointState::Error] - .into_iter() - .find(|state| !self.ruled_out(*state)) - } -} - -/// One quiesce of a bulk pair, as the image of both endpoints and the looks -/// still left. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Quiescing { - looks: u8, - pipes: [Image; 2], - issued: Option<(Command, Pipe, EndpointState)>, - ended: Option, -} - -impl Quiescing { - /// `broke` is the pipe and completion code of the transfer event that - /// ended the round trip, where one did; a break with no event (silence, a - /// malformed status block) starts from the fields alone. - pub fn begin(broke: Option<(Pipe, u32)>) -> Self { - let mut pipes = [Image::BLANK; 2]; - if let Some((pipe, code)) = broke { - pipes[pipe.index()].known = event_state(code); - } - Self { looks: 0, pipes, issued: None, ended: None } - } - - /// The next command that takes the pair towards Stopped, Bulk-In first. - pub fn look(&mut self, in_field: EndpointState, out_field: EndpointState) -> Look { - if let Some(why) = self.ended { - return Look::GaveUp(why); - } - if self.looks == MOST_LOOKS { - return self.give_up(GaveUp::OutOfLooks); - } - self.looks += 1; - for (pipe, field) in [(Pipe::In, in_field), (Pipe::Out, out_field)] { - let Some(state) = self.pipes[pipe.index()].believed(field) else { - return self.give_up(GaveUp::Contradicted(pipe)); - }; - let cmd = match state { - EndpointState::Halted => Command::ResetEndpoint, - EndpointState::Running => Command::StopEndpoint, - EndpointState::Error => Command::SetDequeue, - EndpointState::Stopped => continue, - EndpointState::Disabled | EndpointState::Unusable(_) => { - return self.give_up(GaveUp::NeedsConfigure(pipe, state)) - } - }; - self.issued = Some((cmd, pipe, state)); - return Look::Command(cmd, pipe); - } - Look::Stopped - } - - /// The controller's answer to the command the last look asked for, or - /// `None` for a controller that gave none. - pub fn answered(&mut self, code: Option) -> Answered { - let (cmd, pipe, from) = self.issued.take().expect("an answer to a command no look asked for"); - let image = &mut self.pipes[pipe.index()]; - match code { - Some(SUCCESS) => { - image.known = Some(EndpointState::Stopped); - Answered::Took - } - Some(CONTEXT_STATE_ERROR) => { - image.known = None; - if let Some(at) = Image::slot(from) { - image.not[at] = true; - } - Answered::Moved { from } - } - Some(code) => { - self.ended = Some(GaveUp::Refused(cmd, code)); - Answered::Ended - } - None => { - self.ended = Some(GaveUp::Silent(cmd)); - Answered::Ended - } - } - } - - fn give_up(&mut self, why: GaveUp) -> Look { - self.ended = Some(why); - Look::GaveUp(why) - } -} - -/// One step of the recovery once both endpoints are Stopped. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Step { - /// Configure Endpoint with both bulk endpoints dropped and added, on fresh - /// rings. - Reconfigure, - /// The Bulk-Only Mass Storage Reset (BOT §3.1, §5.3.4 (a)). - MassStorageReset, - /// ClearFeature(ENDPOINT_HALT) on one pipe (§5.3.4 (b) and (c)). - ClearHalt(Pipe), -} - -/// What follows the quiesce, in the order it is taken: the one command left, -/// then the class's three requests. -pub const AFTER_QUIESCE: [Step; 4] = [ - Step::Reconfigure, - Step::MassStorageReset, - Step::ClearHalt(Pipe::In), - Step::ClearHalt(Pipe::Out), -]; - -/// Who gives back the slot of a disk that is given up on while it is still -/// plugged in. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum SlotGoes { - /// Back to the controller now, by Disable Slot. - Back, - /// Nowhere until the device leaves its port: the port keeps holding it and - /// its teardown gives it back. - WithTheUnplug, -} - -/// Whether a pair has nothing on it for a quiesce to stop: both Disabled, which -/// is how a Reset Device leaves every endpoint but the control endpoint (xHCI -/// 1.2 §4.6.11). -pub fn nothing_to_stop(in_field: EndpointState, out_field: EndpointState) -> bool { - in_field == EndpointState::Disabled && out_field == EndpointState::Disabled -} - -/// Disable Slot is defined over endpoints that are Stopped, or Running with -/// nothing to run (xHCI 1.2 §4.6.4's note). A pair the quiesce could not take -/// to Stopped keeps its slot, inside a bind as much as after one. -pub fn slot_after_offline(pair_stopped: bool) -> SlotGoes { - if pair_stopped { - SlotGoes::Back - } else { - SlotGoes::WithTheUnplug - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const RECOVERABLE: [EndpointState; 4] = [ - EndpointState::Halted, - EndpointState::Running, - EndpointState::Stopped, - EndpointState::Error, - ]; - - /// xHCI 1.2 §4.8.3's endpoint state machine for the three commands a - /// quiesce issues: the state each leaves, or `None` for the Context State - /// Error every other state answers with. - fn controller(state: EndpointState, cmd: Command) -> Option { - match (cmd, state) { - (Command::ResetEndpoint, EndpointState::Halted) - | (Command::StopEndpoint, EndpointState::Running) - | (Command::SetDequeue, EndpointState::Stopped | EndpointState::Error) => { - Some(EndpointState::Stopped) - } - _ => None, - } - } - - /// A pair as the model holds it: where each endpoint is, what its field - /// reads, and where an abandoned transfer's error moves it between the - /// look that finds it Running and the command chosen from that. - #[derive(Clone, Copy)] - struct Pair { - actual: [EndpointState; 2], - field: [EndpointState; 2], - moves_under: [Option; 2], - /// A field that lags is never written again: §4.8.3's note puts no - /// bound on the deferral. - lags: bool, - } - - impl Pair { - fn current(in_state: EndpointState, out_state: EndpointState) -> Self { - let actual = [in_state, out_state]; - Self { actual, field: actual, moves_under: [None; 2], lags: false } - } - } - - /// What a walk came to: the looks taken to Stopped, and the commands the - /// controller refused on the way. - #[derive(Debug, PartialEq, Eq)] - struct Walked { - looks: u8, - refused: u8, - } - - /// Run `plan` against the model until it stops asking. `Err` is the give-up. - fn walk(mut plan: Quiescing, mut pair: Pair) -> Result { - let (mut looks, mut refused) = (0, 0); - loop { - looks += 1; - assert!(looks <= MOST_LOOKS + 1, "a quiesce that outlives its own bound"); - match plan.look(pair.field[0], pair.field[1]) { - Look::Stopped => { - assert_eq!(pair.actual, [EndpointState::Stopped; 2], "called Stopped and is not"); - return Ok(Walked { looks, refused }); - } - Look::GaveUp(why) => return Err(why), - Look::Command(cmd, pipe) => { - let at = pipe.index(); - if pair.actual[at] == EndpointState::Running { - if let Some(to) = pair.moves_under[at].take() { - pair.actual[at] = to; - } - } - let code = match controller(pair.actual[at], cmd) { - Some(next) => { - pair.actual[at] = next; - SUCCESS - } - None => { - refused += 1; - CONTEXT_STATE_ERROR - } - }; - if !pair.lags { - pair.field[at] = pair.actual[at]; - } - plan.answered(Some(code)); - } - } - } - } - - fn every_pair() -> impl Iterator { - RECOVERABLE - .into_iter() - .flat_map(|a| RECOVERABLE.into_iter().map(move |b| (a, b))) - } - - const MOVES: [Option; 3] = - [None, Some(EndpointState::Halted), Some(EndpointState::Error)]; - - /// With a field that is current, every command a look yields is one - /// §4.8.3 defines for the state that look saw, so a Context State Error is - /// only ever the state having moved. - #[test] - fn every_command_is_defined_for_the_state_it_was_chosen_from() { - for (a, b) in every_pair() { - let walked = walk(Quiescing::begin(None), Pair::current(a, b)).expect("recoverable"); - assert_eq!(walked.refused, 0, "{a:?}/{b:?}"); - } - } - - /// Whatever pair a break leaves, wherever an error moves either pipe under - /// its look, and whether or not the field ever says so, the pair is - /// Stopped inside the bound. - #[test] - fn every_pair_is_stopped_inside_the_bound_however_it_moves_and_however_the_field_lags() { - for (a, b) in every_pair() { - for in_moves in MOVES { - for out_moves in MOVES { - for lags in [false, true] { - let pair = Pair { - moves_under: [in_moves, out_moves], - lags, - ..Pair::current(a, b) - }; - let walked = walk(Quiescing::begin(None), pair); - assert!( - walked.is_ok(), - "{a:?}/{b:?} moving {in_moves:?}/{out_moves:?} lags={lags}: {walked:?}" - ); - } - } - } - } - } - - /// The bound is the worst case and not a margin over it: both pipes - /// Running, both moved to Error under their look, neither field saying so. - #[test] - fn the_bound_is_the_worst_case() { - let mut worst = 0; - for in_moves in MOVES { - for out_moves in MOVES { - for lags in [false, true] { - let pair = Pair { - moves_under: [in_moves, out_moves], - lags, - ..Pair::current(EndpointState::Running, EndpointState::Running) - }; - worst = worst.max(walk(Quiescing::begin(None), pair).expect("recoverable").looks); - } - } - } - assert_eq!(worst, MOST_LOOKS); - } - - /// §4.8.3's note, the case a plan that reads the field goes wrong on: the - /// transfer event said Stall, Babble or Transaction Error, the endpoint is - /// Halted, and the field still reads Running. The event's word is taken: - /// Reset Endpoint first, no command refused, and the pair Stopped in three - /// looks — where choosing from the field spends every look on a Stop - /// Endpoint the controller refuses, and takes a healthy disk offline. - #[test] - fn a_halting_completion_code_outranks_a_field_that_still_reads_running() { - for code in [3, 4, 6] { - for pipe in [Pipe::In, Pipe::Out] { - let mut pair = Pair::current(EndpointState::Running, EndpointState::Running); - pair.actual[pipe.index()] = EndpointState::Halted; - pair.lags = true; - let mut plan = Quiescing::begin(Some((pipe, code))); - let first = plan.look(pair.field[0], pair.field[1]); - if pipe == Pipe::In { - assert_eq!(first, Look::Command(Command::ResetEndpoint, Pipe::In), "code {code}"); - } - let walked = walk(Quiescing::begin(Some((pipe, code))), pair).expect("recoverable"); - assert_eq!(walked, Walked { looks: 3, refused: 0 }, "code {code} on {pipe:?}"); - } - } - } - - /// A command that succeeded is an event too: its pipe is Stopped whatever - /// the field goes on reading, so it is never commanded twice. - #[test] - fn a_command_that_took_is_not_issued_again_over_a_field_that_lags() { - let pair = Pair { lags: true, ..Pair::current(EndpointState::Halted, EndpointState::Running) }; - let walked = walk(Quiescing::begin(None), pair).expect("recoverable"); - assert_eq!(walked, Walked { looks: 3, refused: 0 }); - } - - /// A Context State Error is the state having moved, never the end of the - /// quiesce: with a current field the next look reads where it went. - #[test] - fn a_context_state_error_is_followed_by_the_command_for_where_the_pipe_went() { - let mut plan = Quiescing::begin(None); - let (running, halted, stopped) = - (EndpointState::Running, EndpointState::Halted, EndpointState::Stopped); - assert_eq!(plan.look(running, stopped), Look::Command(Command::StopEndpoint, Pipe::In)); - assert_eq!(plan.answered(Some(CONTEXT_STATE_ERROR)), Answered::Moved { from: running }); - assert_eq!(plan.look(halted, stopped), Look::Command(Command::ResetEndpoint, Pipe::In)); - assert_eq!(plan.answered(Some(SUCCESS)), Answered::Took); - assert_eq!(plan.look(halted, stopped), Look::Stopped); - } - - /// Any other refusal, and a controller that does not answer, end it: the - /// next look owes no command. - #[test] - fn a_refusal_that_is_not_about_state_and_a_silence_both_end_the_quiesce() { - let running = EndpointState::Running; - for (code, why) in [ - (Some(5), GaveUp::Refused(Command::StopEndpoint, 5)), - (None, GaveUp::Silent(Command::StopEndpoint)), - ] { - let mut plan = Quiescing::begin(None); - assert_eq!(plan.look(running, running), Look::Command(Command::StopEndpoint, Pipe::In)); - assert_eq!(plan.answered(code), Answered::Ended); - assert_eq!(plan.look(running, running), Look::GaveUp(why)); - assert_eq!(plan.look(running, running), Look::GaveUp(why), "and stays ended"); - } - } - - /// A controller that refuses every command its states define is not asked - /// a fourth time. - #[test] - fn a_pipe_refused_from_every_state_is_given_up_on_by_name() { - let mut plan = Quiescing::begin(None); - let running = EndpointState::Running; - for cmd in [Command::StopEndpoint, Command::ResetEndpoint, Command::SetDequeue] { - assert_eq!(plan.look(running, running), Look::Command(cmd, Pipe::In)); - plan.answered(Some(CONTEXT_STATE_ERROR)); - } - assert_eq!(plan.look(running, running), Look::GaveUp(GaveUp::Contradicted(Pipe::In))); - } - - /// A pair with nothing to move costs one look and no command. - #[test] - fn a_stopped_pair_is_asked_nothing() { - let stopped = EndpointState::Stopped; - assert_eq!(Quiescing::begin(None).look(stopped, stopped), Look::Stopped); - } - - /// Bulk-In first, so the order the record shows is the order taken. - #[test] - fn the_in_pipe_is_quiesced_before_the_out_pipe() { - assert_eq!( - Quiescing::begin(None).look(EndpointState::Halted, EndpointState::Running), - Look::Command(Command::ResetEndpoint, Pipe::In) - ); - assert_eq!( - Quiescing::begin(None).look(EndpointState::Stopped, EndpointState::Running), - Look::Command(Command::StopEndpoint, Pipe::Out) - ); - } - - /// An endpoint in a state no command leaves is refused by name, naming the - /// pipe; the other pipe's state does not rescue it, and neither does an - /// event. - #[test] - fn a_disabled_or_reserved_endpoint_refuses_the_whole_recovery() { - let stopped = EndpointState::Stopped; - for bad in [EndpointState::Disabled, EndpointState::Unusable(5), EndpointState::Unusable(7)] - { - for broke in [None, Some((Pipe::In, 6)), Some((Pipe::Out, 6))] { - assert_eq!( - Quiescing::begin(broke).look(bad, stopped), - Look::GaveUp(GaveUp::NeedsConfigure(Pipe::In, bad)), - "{bad:?} in, {broke:?}" - ); - } - assert_eq!( - Quiescing::begin(None).look(stopped, bad), - Look::GaveUp(GaveUp::NeedsConfigure(Pipe::Out, bad)), - "{bad:?} out" - ); - } - } - - /// Table 6-90's numbers against §4.8.3's two lists; a code that reports no - /// state leaves the pipe to its field. - #[test] - fn only_a_halt_condition_or_a_trb_error_speaks_for_the_endpoint() { - for code in 0..=255 { - let want = match code { - 3 | 4 | 6 | 36 => Some(EndpointState::Halted), - 5 => Some(EndpointState::Error), - _ => None, - }; - assert_eq!(event_state(code), want, "code {code}"); - } - // Short Packet is a transfer that ended early, not an endpoint that - // stopped. - let mut plan = Quiescing::begin(Some((Pipe::In, 13))); - assert_eq!( - plan.look(EndpointState::Running, EndpointState::Stopped), - Look::Command(Command::StopEndpoint, Pipe::In) - ); - } - - #[test] - fn only_a_pair_a_reset_device_disabled_has_nothing_to_stop() { - let disabled = EndpointState::Disabled; - assert!(nothing_to_stop(disabled, disabled)); - for other in RECOVERABLE { - assert!(!nothing_to_stop(other, disabled), "{other:?} in"); - assert!(!nothing_to_stop(disabled, other), "{other:?} out"); - } - } - - /// §4.6.4's note, for the bound disk and the one still inside its bind - /// alike: the slot goes back only over a Stopped pair. - #[test] - fn a_slot_goes_back_only_over_a_stopped_pair() { - assert_eq!(slot_after_offline(true), SlotGoes::Back); - assert_eq!(slot_after_offline(false), SlotGoes::WithTheUnplug); - } -} diff --git a/lib/xhci/src/ring.rs b/lib/xhci/src/ring.rs deleted file mode 100644 index c7a3fbce805..00000000000 --- a/lib/xhci/src/ring.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! Where a transfer ring stands: the controller's dequeue pointer against the -//! driver's own enqueue point. -//! -//! The controller says how far it has got in the endpoint's output context -//! (xHCI 1.2 Table 6-8, dwords 2-3) as an *address* on the ring, never an index -//! into it. Turning that back into a position is arithmetic over a number the -//! driver did not write, so it is here and it refuses by name: the one reader -//! is a reset path that may not panic, and a pointer that is no position on -//! this ring has to be said rather than wrapped into a plausible count. - -/// One TRB's width (xHCI 1.2 §4.11.1), which is also the alignment every -/// address on a ring has. -const TRB_BYTES: u64 = 16; - -/// One transfer ring, as the numbers a position is computed from. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Ring { - /// The ring's first TRB, in the address space the controller was programmed - /// in. - pub base: u64, - /// TRBs the ring holds, its link TRB included. - pub trbs: u16, - /// Where the driver's next enqueue goes, as an index into the ring. - pub tail: u16, -} - -/// Why a dequeue pointer says nothing about how far a controller had got. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum NotOnTheRing { - /// Before the ring's first TRB, or past its last — which is what a context - /// the controller never wrote, or one published mid-update, reads as. - Dequeue(u64), - /// The driver's own next enqueue point is past the end of its ring. - Tail(u16), -} - -impl core::fmt::Display for NotOnTheRing { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Dequeue(at) => write!(f, "a dequeue pointer of {at:#x}, which is no TRB on that ring"), - Self::Tail(tail) => write!(f, "an enqueue point of {tail}, which is past the end of that ring"), - } - } -} - -impl Ring { - /// TRBs the driver has queued that the controller has not reached, out of - /// the Endpoint Context's TR Dequeue Pointer field. - /// - /// The field's low four bits are the Dequeue Cycle State and three reserved - /// bits (xHCI 1.2 §6.2.3) and not address, so they are masked off before - /// anything is decided about where it points. - pub fn pending(self, dequeue: u64) -> Result { - if self.tail >= self.trbs { - return Err(NotOnTheRing::Tail(self.tail)); - } - let at = dequeue & !(TRB_BYTES - 1); - let Some(off) = at.checked_sub(self.base) else { - return Err(NotOnTheRing::Dequeue(dequeue)); - }; - let trbs = u64::from(self.trbs); - // A base that is not itself a TRB boundary puts every position on this - // ring between two, which is no TRB on it either. - if !off.is_multiple_of(TRB_BYTES) || off / TRB_BYTES >= trbs { - return Err(NotOnTheRing::Dequeue(dequeue)); - } - let reached = off / TRB_BYTES; - // Ring positions and not addresses: the driver's tail is ahead of the - // controller's, and the distance between them wraps with the ring. - Ok(((u64::from(self.tail) + trbs - reached) % trbs) as u16) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// A bulk ring as this tree builds one: a page of TRBs at a page-aligned - /// address. - fn ring(tail: u16) -> Ring { - Ring { base: 0x1_0000, trbs: 256, tail } - } - - /// The address of ring index `at`. - fn trb(at: u64) -> u64 { - 0x1_0000 + at * 16 - } - - #[test] - fn a_controller_that_has_caught_up_has_nothing_pending() { - for at in [0, 1, 17, 200, 255] { - assert_eq!(ring(at as u16).pending(trb(at)), Ok(0), "index {at}"); - } - } - - /// The count is the distance from where the controller is to where the - /// driver's next TRB goes, and it wraps: a dequeue pointer *ahead* of the - /// tail is the ordinary state of a ring that has been round once. - #[test] - fn the_count_is_the_wrapped_distance_between_the_two() { - for (tail, reached, want) in - [(20u64, 0u64, 20u16), (0, 20, 236), (255, 254, 1), (10, 200, 66), (0, 1, 255)] - { - assert_eq!( - ring(tail as u16).pending(trb(reached)), - Ok(want), - "tail {tail}, reached {reached}" - ); - } - } - - /// The Dequeue Cycle State and the three bits beside it ride in the same - /// field; a decode that took them for address would answer about a TRB the - /// controller is not on. - #[test] - fn the_low_four_bits_of_the_field_are_not_address() { - for noise in 0..16u64 { - assert_eq!(ring(20).pending(trb(0) | noise), Ok(20), "low bits {noise}"); - } - } - - /// **The refusal this exists for.** A context the controller never wrote, - /// or one read while another CPU was publishing it, hands back a word that - /// is not on this ring at all. - #[test] - fn a_pointer_that_is_no_position_on_this_ring_is_refused_by_name() { - let r = ring(20); - for stray in [0, 0xF, 0x8000, trb(0) - 16, trb(256), trb(4096), u64::MAX] { - assert_eq!(r.pending(stray), Err(NotOnTheRing::Dequeue(stray)), "{stray:#x}"); - } - // The two ends, either side of the boundary: the last TRB is on the - // ring and the one after it is not. - assert_eq!(r.pending(trb(255)), Ok(21)); - assert_eq!(r.pending(trb(256)), Err(NotOnTheRing::Dequeue(trb(256)))); - } - - /// The driver's own half of the pair, refused for the same reason: a tail - /// off the end of the ring makes the distance meaningless, and a modulo - /// would hide it. - #[test] - fn an_enqueue_point_past_the_end_of_the_ring_is_refused() { - for tail in [256u16, 257, u16::MAX] { - let r = Ring { base: 0x1_0000, trbs: 256, tail }; - assert_eq!(r.pending(trb(0)), Err(NotOnTheRing::Tail(tail))); - } - // And a ring of no TRBs is that same refusal rather than a division by - // zero. - assert_eq!( - Ring { base: 0x1_0000, trbs: 0, tail: 0 }.pending(trb(0)), - Err(NotOnTheRing::Tail(0)) - ); - } - - /// Each refusal says which of the two it is, because the account is the - /// only place a reader learns anything about that reset at all. - #[test] - fn no_two_refusals_read_the_same() { - extern crate std; - use std::string::ToString; - let said = [ - NotOnTheRing::Dequeue(0x20).to_string(), - NotOnTheRing::Tail(300).to_string(), - ]; - for (at, one) in said.iter().enumerate() { - assert!(!one.is_empty()); - for other in &said[at + 1..] { - assert_ne!(one, other); - } - } - } -} diff --git a/lib/xhci/src/scan.rs b/lib/xhci/src/scan.rs deleted file mode 100644 index ab28c7baf7e..00000000000 --- a/lib/xhci/src/scan.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! What the boot scan does next with the controller's one operation slot. -//! -//! The scan runs before there is a scheduler, so its loop is written in place -//! and every step of it is a wait. Two things can hold it: the operation the -//! controller owes an answer for ([`job::Outstanding`](crate::job::Outstanding) -//! is one slot), and an endpoint a completion code broke that no recovery has -//! taken yet. They end differently, and that difference is this module. -//! -//! **An outstanding operation is never abandoned here.** The slot is the -//! scan's only channel: the next port's Enable Slot goes into it, and a scan -//! that returned while one was still in flight would leave the port after it -//! submitting over a live operation. The slot empties on its own — every -//! operation carries a deadline and a pass that finds it expired takes the -//! operation as silent — so waiting for it is bounded by that deadline and -//! needs no bound of its own. -//! -//! **A broken endpoint has no deadline**, which is why the scan carries a quiet -//! bound at all: `recover_endpoints` can only start while the slot is free, a -//! device can break again the moment it is recovered, and nothing in that says -//! when to stop. The bound is silence — re-armed by every event the controller -//! produces — and it is spendable only on this half. - -use crate::port::Nanos; - -/// What the scan's loop does with what it has just seen. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Settle { - /// Nothing is outstanding and no endpoint is broken: the scan may go on to - /// the next port, which is the only state in which its slot is free. - Done, - /// Keep draining. The controller owes an answer, and its own deadline is - /// what ends this. - Wait, - /// Start the recovery a broken endpoint is owed; the slot is free for it. - Recover, - /// Nothing has been heard for the whole bound and the endpoint is still - /// broken. The slot is free, so the scan goes on without it. - GaveUp, -} - -/// The scan's next step, given whether the controller owes an answer -/// (`busy`), whether a completion code left an endpoint broken (`broken`), and -/// where the silence bound stands. -/// -/// `busy` decides first and alone: see the module header. -pub fn settle(busy: bool, broken: bool, now: Nanos, quiet_until: Nanos) -> Settle { - if busy { - return Settle::Wait; - } - if !broken { - return Settle::Done; - } - if now >= quiet_until { - return Settle::GaveUp; - } - Settle::Recover -} - -#[cfg(test)] -mod tests { - use super::*; - - /// **The defect this file exists for.** A boot scan that gave up while the - /// controller still owed an answer left its one slot occupied, and the next - /// port's `device::begin` submitted an Enable Slot over it — which is a - /// panic in `job::Outstanding::submit`, reached by nothing worse than a - /// stick that did not answer its first command. No `busy` state has any - /// answer but [`Settle::Wait`], whatever the clock says. - #[test] - fn an_outstanding_operation_is_never_given_up_on() { - for broken in [false, true] { - for now in [0, 99, 100, 101, Nanos::MAX] { - assert_eq!(settle(true, broken, now, 100), Settle::Wait, "{broken} {now}"); - } - } - } - - /// The scan's ordinary end: the slot is free and no device is owed a - /// recovery, whether or not the bound has passed. - #[test] - fn a_free_slot_and_nothing_broken_is_the_end_of_the_scan() { - assert_eq!(settle(false, false, 0, 100), Settle::Done); - assert_eq!(settle(false, false, Nanos::MAX, 100), Settle::Done); - } - - /// The half the bound is for: a broken endpoint is recovered while the - /// controller is still being heard from, and left once it is not. - #[test] - fn a_broken_endpoint_is_recovered_until_the_silence_bound_passes() { - assert_eq!(settle(false, true, 0, 100), Settle::Recover); - assert_eq!(settle(false, true, 99, 100), Settle::Recover); - assert_eq!(settle(false, true, 100, 100), Settle::GaveUp); - assert_eq!(settle(false, true, Nanos::MAX, 100), Settle::GaveUp); - } - - /// The loop as a whole, over a controller that answers nothing: the slot - /// empties on its deadline and the scan ends, so a bound already spent - /// before the operation was even submitted costs no correctness. - #[test] - fn a_silent_controller_ends_the_scan_at_the_operations_deadline() { - // The bound was spent by a long blocking bind, as it is on the machine. - let quiet_until = 0; - let deadline = 2_000; - let mut now = 0; - loop { - let busy = now < deadline; - match settle(busy, false, now, quiet_until) { - Settle::Wait => now += 1, - Settle::Done => break, - other => panic!("{other:?} at {now}"), - } - assert!(now <= deadline, "the scan waited past the operation's deadline"); - } - assert_eq!(now, deadline); - } -} diff --git a/sdk/abi/src/input.rs b/sdk/abi/src/input.rs index 2e23438492f..71a7986a309 100644 --- a/sdk/abi/src/input.rs +++ b/sdk/abi/src/input.rs @@ -1,7 +1,5 @@ -//! Wire-format types for HID input devices (keyboard, mouse). -//! -//! These cross the kernel→userland boundary as reads on a device handle, and -//! a source's connection to the owner of input (`toyos-input`). +//! A key transition and a pointer's axes, as the surface protocol and the +//! owner of input (`toyos-input`) carry them. /// Keyboard modifier flags. pub const MOD_SHIFT: u8 = 1; @@ -36,31 +34,10 @@ 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. +/// A pointer report's 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 +/// A pointer report's 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! { - /// 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, - /// [`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/abi/src/inventory.rs b/sdk/abi/src/inventory.rs index d5096f951aa..1cbad8920be 100644 --- a/sdk/abi/src/inventory.rs +++ b/sdk/abi/src/inventory.rs @@ -91,117 +91,12 @@ pub struct Pci { pub driven: Driven, } -/// What a USB device was bound as. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum UsbFunction { - Keyboard, - /// A mouse or a tablet: the kernel dispatches the two identically. - Pointer, - Storage, -} - -/// The speed a USB port trained at: PORTSC's Port Speed under the default -/// Protocol Speed IDs (xHCI 1.2 §7.2.2.1.1, Table 7-13), which are the only -/// ones a device this kernel binds can have. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum UsbSpeed { - Full, - Low, - High, - /// SuperSpeed, Gen 1 on one lane. - Super, - /// SuperSpeedPlus, Gen 2 on one lane. - SuperPlusGen2x1, - /// SuperSpeedPlus, Gen 1 on two lanes. - SuperPlusGen1x2, - /// SuperSpeedPlus, Gen 2 on two lanes. - SuperPlusGen2x2, -} - -impl UsbSpeed { - /// The Port Speed value, or `None` for one no default ID names. - pub fn from_psiv(psiv: u8) -> Option { - Some(match psiv { - 1 => Self::Full, - 2 => Self::Low, - 3 => Self::High, - 4 => Self::Super, - 5 => Self::SuperPlusGen2x1, - 6 => Self::SuperPlusGen1x2, - 7 => Self::SuperPlusGen2x2, - _ => return None, - }) - } - - pub fn psiv(self) -> u8 { - match self { - Self::Full => 1, - Self::Low => 2, - Self::High => 3, - Self::Super => 4, - Self::SuperPlusGen2x1 => 5, - Self::SuperPlusGen1x2 => 6, - Self::SuperPlusGen2x2 => 7, - } - } -} - -/// One USB device a kernel driver bound. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Usb { - /// The xHCI controller it hangs off. - pub controller: PciAddr, - /// The root-hub port, from 1. - pub port: u8, - pub speed: UsbSpeed, - pub vendor: u16, - pub product: u16, - pub function: UsbFunction, -} - -/// One block device. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Block { - pub device: u32, - /// In [`crate::part::BLOCK_BYTES`] blocks. - pub blocks: u64, -} - -/// Who holds a partition's blocks, as the block layer records every hold. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum PartState { - /// No hold is exactly this partition's span. - Free, - /// This kernel holds it: a filesystem it mounted, or a probe of its own. - Kernel, - /// A process's partition claim holds it; a [`Claim`] record says whose, - /// when the handle was in a table. - Claimed, -} - -/// One GPT entry, as its table stated it when the disk was probed. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Partition { - pub device: u32, - /// Its entry's index in the table. - pub index: u32, - pub type_guid: [u8; 16], - pub unique_guid: [u8; 16], - /// In the device's own logical blocks, as GPT stores them. - pub first_lba: u64, - pub lbas: u64, - pub state: PartState, -} - /// What a claim is on. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Claimed { /// A device class. Class(DeviceType), Pci(PciAddr), - /// A partition, by the block device it is on and its unique GUID: a - /// GUID a table carries twice is refused a claim, so the pair names one. - Partition { device: u32, unique_guid: [u8; 16] }, } /// A claim, and the process holding its handle. @@ -236,9 +131,6 @@ pub struct Loaded { #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Record { Pci(Pci), - Usb(Usb), - Block(Block), - Partition(Partition), Claim(Claim), Loaded(Loaded), } @@ -265,9 +157,6 @@ impl core::fmt::Display for Undecodable { } const KIND_PCI: u8 = 1; -const KIND_USB: u8 = 2; -const KIND_BLOCK: u8 = 3; -const KIND_PARTITION: u8 = 4; const KIND_CLAIM: u8 = 5; const KIND_LOADED: u8 = 6; @@ -354,44 +243,6 @@ impl Record { ); o.u32(16, p.arrival.0); } - Self::Usb(u) => { - o.u8(0, KIND_USB); - o.addr(1, u.controller); - o.u8(6, u.port); - o.u8(7, u.speed.psiv()); - o.u16(8, u.vendor); - o.u16(10, u.product); - o.u8( - 12, - match u.function { - UsbFunction::Keyboard => 0, - UsbFunction::Pointer => 1, - UsbFunction::Storage => 2, - }, - ); - } - Self::Block(b) => { - o.u8(0, KIND_BLOCK); - o.u32(4, b.device); - o.u64(8, b.blocks); - } - Self::Partition(p) => { - o.u8(0, KIND_PARTITION); - o.u8( - 1, - match p.state { - PartState::Free => 0, - PartState::Kernel => 1, - PartState::Claimed => 2, - }, - ); - o.u32(4, p.device); - o.u64(8, p.first_lba); - o.u64(16, p.lbas); - o.bytes(24, &p.type_guid); - o.bytes(40, &p.unique_guid); - o.u32(56, p.index); - } Self::Claim(c) => { o.u8(0, KIND_CLAIM); match c.on { @@ -403,11 +254,6 @@ impl Record { o.u8(1, 1); o.addr(4, at); } - Claimed::Partition { device, unique_guid } => { - o.u8(1, 2); - o.u32(4, device); - o.bytes(8, &unique_guid); - } } holder_out(&mut o, 28, &c.holder); } @@ -446,36 +292,8 @@ impl Record { _ => None, })?, }), - KIND_USB => Self::Usb(Usb { - controller: r.addr(1), - port: r.u8(6), - speed: r.variant(7, UsbSpeed::from_psiv)?, - vendor: r.u16(8), - product: r.u16(10), - function: r.variant(12, |b| match b { - 0 => Some(UsbFunction::Keyboard), - 1 => Some(UsbFunction::Pointer), - 2 => Some(UsbFunction::Storage), - _ => None, - })?, - }), - KIND_BLOCK => Self::Block(Block { device: r.u32(4), blocks: r.u64(8) }), - KIND_PARTITION => Self::Partition(Partition { - state: r.variant(1, |b| match b { - 0 => Some(PartState::Free), - 1 => Some(PartState::Kernel), - 2 => Some(PartState::Claimed), - _ => None, - })?, - device: r.u32(4), - first_lba: r.u64(8), - lbas: r.u64(16), - type_guid: r.array(24), - unique_guid: r.array(40), - index: r.u32(56), - }), KIND_CLAIM => Self::Claim(Claim { - on: match r.variant(1, |b| (b <= 2).then_some(b))? { + on: match r.variant(1, |b| (b <= 1).then_some(b))? { 0 => { let raw = r.u64(4); Claimed::Class( @@ -483,8 +301,7 @@ impl Record { .ok_or(Undecodable::Variant { at: 4, value: raw })?, ) } - 1 => Claimed::Pci(r.addr(4)), - _ => Claimed::Partition { device: r.u32(4), unique_guid: r.array(8) }, + _ => Claimed::Pci(r.addr(4)), }, holder: holder_in(&r, 28), }), @@ -513,7 +330,7 @@ mod tests { n } - fn every_kind() -> [Record; 8] { + fn every_kind() -> [Record; 4] { let at = PciAddr { segment: 0x1234, bus: 0, dev: 0x1f, func: 6 }; let holder = Holder { pid: 7, name: name("netstack") }; [ @@ -527,30 +344,8 @@ mod tests { prog_if: 0, driven: Driven::Claimed, }), - Record::Usb(Usb { - controller: PciAddr { segment: 0, bus: 0, dev: 4, func: 0 }, - port: 3, - speed: UsbSpeed::SuperPlusGen2x2, - vendor: 0x0627, - product: 1, - function: UsbFunction::Pointer, - }), - Record::Block(Block { device: 1, blocks: u64::MAX }), - Record::Partition(Partition { - device: 1, - index: 128, - type_guid: [0xab; 16], - unique_guid: [0xcd; 16], - first_lba: 2048, - lbas: 4096, - state: PartState::Kernel, - }), Record::Claim(Claim { on: Claimed::Pci(at), holder }), - Record::Claim(Claim { on: Claimed::Class(DeviceType::Mouse), holder }), - Record::Claim(Claim { - on: Claimed::Partition { device: 3, unique_guid: [0x11; 16] }, - holder, - }), + Record::Claim(Claim { on: Claimed::Class(DeviceType::Framebuffer), holder }), Record::Loaded(Loaded { role: Role::Log, unique_guid: [0x22; 16] }), ] } @@ -561,11 +356,6 @@ mod tests { assert_eq!(Record::decode(&record.encode()), Ok(record)); } assert_eq!(Holder { pid: 1, name: name("soundserver") }.name(), Some("soundserver")); - for psiv in 0..=u8::MAX { - if let Some(speed) = UsbSpeed::from_psiv(psiv) { - assert_eq!(speed.psiv(), psiv); - } - } } #[test] @@ -574,34 +364,14 @@ mod tests { let mut raw = RawRecord::EMPTY; raw.0[0] = 99; assert_eq!(Record::decode(&raw), Err(Undecodable::Kind(99))); - for (kind, at) in [(KIND_PCI, 13), (KIND_USB, 12), (KIND_PARTITION, 1), (KIND_CLAIM, 1)] { + for (kind, at) in [(KIND_PCI, 13), (KIND_CLAIM, 1), (KIND_LOADED, 1)] { let mut raw = RawRecord::EMPTY; raw.0[0] = kind; raw.0[at] = 9; - if kind == KIND_USB { - raw.0[7] = 1; - } assert_eq!(Record::decode(&raw), Err(Undecodable::Variant { at, value: 9 }), "{kind}"); } - // A speed no default Protocol Speed ID names. - let mut raw = Record::Usb(Usb { - controller: PciAddr { segment: 0, bus: 0, dev: 4, func: 0 }, - port: 1, - speed: UsbSpeed::Full, - vendor: 0, - product: 0, - function: UsbFunction::Keyboard, - }) - .encode(); - for psiv in [0, 8, 15] { - raw.0[7] = psiv; - assert_eq!( - Record::decode(&raw), - Err(Undecodable::Variant { at: 7, value: u64::from(psiv) }) - ); - } let mut raw = Record::Claim(Claim { - on: Claimed::Class(DeviceType::Keyboard), + on: Claimed::Class(DeviceType::Framebuffer), holder: Holder { pid: 1, name: name("x") }, }) .encode(); diff --git a/sdk/abi/src/lib.rs b/sdk/abi/src/lib.rs index b71b9323774..5686be024e8 100644 --- a/sdk/abi/src/lib.rs +++ b/sdk/abi/src/lib.rs @@ -26,7 +26,6 @@ pub mod inbox; pub mod input; pub mod inventory; pub mod log; -pub mod part; pub mod pci; pub mod ring; pub mod syscall; diff --git a/sdk/abi/src/part.rs b/sdk/abi/src/part.rs deleted file mode 100644 index 9b82d9a5101..00000000000 --- a/sdk/abi/src/part.rs +++ /dev/null @@ -1,185 +0,0 @@ -//! What a claim on one GPT partition names and hands its holder. -//! -//! A partition is claimed the way a PCI function is: the supervisor mints it from a -//! `system.toml` `devices` entry and endows it, and the holder reads and writes -//! the partition's blocks through [`syscall::partition_read`] and -//! [`syscall::partition_write`], durable at [`syscall::fsync`] on the claim. -//! Every block number here is the **partition's own**, from 0: nothing in this -//! ABI names a device block, so a neighbour's blocks have no spelling. -//! -//! A partition has one holder. Which partitions the kernel holds itself, and so -//! refuses, is `kernel/src/block.rs`'s account. -//! -//! [`syscall::partition_read`]: crate::syscall::partition_read -//! [`syscall::partition_write`]: crate::syscall::partition_write -//! [`syscall::fsync`]: crate::syscall::fsync - -/// The unit every partition transfer is in, and the alignment a partition must -/// have on its device to be claimable at all: a partition that began or ended -/// inside one of these would share a transfer with its neighbour. -pub const BLOCK_BYTES: usize = 4096; - -/// One partition block, as the buffers of [`crate::syscall::partition_read`] -/// and [`crate::syscall::partition_write`] are typed: a partial block has no -/// spelling. -pub type Block = [u8; BLOCK_BYTES]; - -/// The most blocks one transfer call carries. -pub const MAX_BLOCKS_PER_CALL: usize = 32; - -/// A GUID as the sixteen bytes a GPT entry stores (UEFI 2.10 §5.3.3), which is -/// also the order the claim syscall carries it in. -/// -/// The text form is the registry form, `XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX` -/// in **uppercase** hex: the first three groups are little-endian integers on -/// the disk and the last two are bytes in order (UEFI 2.10 Appendix A). One -/// spelling per partition, because every reader of a `devices` entry compares -/// the string: a lowercase twin would be a second name for one partition. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct PartGuid(pub [u8; 16]); - -/// Where each byte of the on-disk form is printed, in text order: the three -/// little-endian groups reversed, the last eight in place. -const TEXT_ORDER: [usize; 16] = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15]; - -/// The dash positions of the 36-byte text form. -const DASHES: [usize; 4] = [8, 13, 18, 23]; - -/// The length of the text form. -pub const GUID_TEXT_LEN: usize = 36; - -const HEX_UPPER: &[u8; 16] = b"0123456789ABCDEF"; - -impl PartGuid { - /// The registry text form, exactly, and nothing else. - pub fn parse(text: &str) -> Option { - let text = text.as_bytes(); - if text.len() != GUID_TEXT_LEN { - return None; - } - let mut digits = [0u8; 32]; - let mut n = 0; - for (at, &byte) in text.iter().enumerate() { - if DASHES.contains(&at) { - if byte != b'-' { - return None; - } - continue; - } - digits[n] = match byte { - b'0'..=b'9' => byte - b'0', - b'A'..=b'F' => byte - b'A' + 10, - _ => return None, - }; - n += 1; - } - let mut bytes = [0u8; 16]; - for (i, &at) in TEXT_ORDER.iter().enumerate() { - bytes[at] = (digits[2 * i] << 4) | digits[2 * i + 1]; - } - Some(Self(bytes)) - } - - /// The registry text form written into `buf`. - pub fn write_text(self, buf: &mut [u8; GUID_TEXT_LEN]) -> &str { - let mut at = 0; - for (i, &byte) in TEXT_ORDER.iter().map(|&b| &self.0[b]).enumerate() { - if i == 4 || i == 6 || i == 8 || i == 10 { - buf[at] = b'-'; - at += 1; - } - buf[at] = HEX_UPPER[(byte >> 4) as usize]; - buf[at + 1] = HEX_UPPER[(byte & 0xF) as usize]; - at += 2; - } - core::str::from_utf8(buf).expect("hex digits and dashes are ASCII") - } - - /// The two selector words the claim syscall carries: bytes 0..8 and - /// 8..16, each little-endian. - pub fn wire(self) -> [u64; 2] { - let mut lo = [0u8; 8]; - let mut hi = [0u8; 8]; - lo.copy_from_slice(&self.0[..8]); - hi.copy_from_slice(&self.0[8..]); - [u64::from_le_bytes(lo), u64::from_le_bytes(hi)] - } - - /// The selector words decoded; every pair of words is some GUID. - pub fn from_wire(words: [u64; 2]) -> Self { - let mut bytes = [0u8; 16]; - bytes[..8].copy_from_slice(&words[0].to_le_bytes()); - bytes[8..].copy_from_slice(&words[1].to_le_bytes()); - Self(bytes) - } -} - -crate::user_safe! { - /// The partition a claim holds, as its holder reads it once off the claim. - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - pub struct PartitionInfo { - /// The partition's length in [`BLOCK_BYTES`] blocks; block numbers - /// `0..blocks` are the whole of what the claim addresses. - pub blocks: u64, - pub unique_guid: [u8; 16], - } -} - -impl PartitionInfo { - /// The partition's unique GUID, as its `part:` entry names it. - pub const fn unique(&self) -> PartGuid { - PartGuid(self.unique_guid) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// The EFI System Partition type's registry text and its on-disk bytes, from - /// the UEFI specification (§5.3.3, Table 5.8): both directions, because a - /// mixed-endian mistake that is self-consistent survives either one alone. - #[test] - fn the_esp_type_guid_is_the_specification_s_bytes() { - const TEXT: &str = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"; - const ON_DISK: [u8; 16] = [ - 0x28, 0x73, 0x2A, 0xC1, 0x1F, 0xF8, 0xD2, 0x11, 0xBA, 0x4B, 0x00, 0xA0, 0xC9, 0x3E, - 0xC9, 0x3B, - ]; - assert_eq!(PartGuid::parse(TEXT), Some(PartGuid(ON_DISK))); - let mut buf = [0u8; GUID_TEXT_LEN]; - assert_eq!(PartGuid(ON_DISK).write_text(&mut buf), TEXT); - } - - /// One spelling per partition: every near miss a config could plausibly - /// write is refused, not normalised. - #[test] - fn a_guid_is_the_uppercase_registry_form_and_nothing_else() { - for bad in [ - "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", - "{C12A7328-F81F-11D2-BA4B-00A0C93EC93B}", - "C12A7328F81F11D2BA4B00A0C93EC93B", - "C12A7328-F81F-11D2-BA4B-00A0C93EC93", - "C12A7328-F81F-11D2-BA4B-00A0C93EC93BB", - "C12A7328-F81F-11D2-BA4B00-A0C93EC93B", - "G12A7328-F81F-11D2-BA4B-00A0C93EC93B", - "C12A7328-F81F-11D2-BA4B-00A0C93EC9é", - "", - ] { - assert_eq!(PartGuid::parse(bad), None, "{bad:?} parsed"); - } - } - - /// The claim carries the sixteen bytes as two words and the kernel takes - /// them apart again; a byte lost to a shift claims a different partition. - #[test] - fn a_guid_survives_the_wire() { - let mut every = [0u8; 16]; - for (i, b) in every.iter_mut().enumerate() { - *b = 0xF0 | i as u8; - } - for guid in [PartGuid(every), PartGuid([0; 16]), PartGuid([0xFF; 16])] { - assert_eq!(PartGuid::from_wire(guid.wire()), guid); - } - } -} diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index 3dc565fbfaa..c506ff7ddd2 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -227,11 +227,8 @@ pub const SYS_DEVICE_BAR_MAP: u64 = 117; /// [`device_dma_alloc`]. pub const SYS_DEVICE_DMA_ALLOC: u64 = 118; -/// Read blocks of a claimed partition. See [`partition_read`]. -pub const SYS_PARTITION_READ: u64 = 119; - -/// Write blocks of a claimed partition. See [`partition_write`]. -pub const SYS_PARTITION_WRITE: u64 = 120; +// 119 and 120 were `SYS_PARTITION_READ` and `SYS_PARTITION_WRITE`, a +// partition claim's blocks on a disk this kernel drove; it drives none. /// What the machine is made of, and who holds each part of it, as /// [`crate::inventory`] records. Gated by [`Rights::INVENTORY`] on a `SysCap`. @@ -555,11 +552,11 @@ pub enum FileType { Unknown = 0, File = 1, Pipe = 2, - Keyboard = 3, + // 3 was `Keyboard` and 7 `Mouse`, the kernel's input streams: input is + // userland's, over the `input` port. Serial = 4, Framebuffer = 5, Tty = 6, - Mouse = 7, Socket = 8, // 9 was `Nic`, the type a claim on the kernel's own NIC driver reported. // A claim on a PCI function reports `Unknown`: what it is, is the driver's @@ -585,11 +582,9 @@ impl FileType { 0 => Some(Self::Unknown), 1 => Some(Self::File), 2 => Some(Self::Pipe), - 3 => Some(Self::Keyboard), 4 => Some(Self::Serial), 5 => Some(Self::Framebuffer), 6 => Some(Self::Tty), - 7 => Some(Self::Mouse), 8 => Some(Self::Socket), 10 => Some(Self::SharedMemory), 11 => Some(Self::Directory), @@ -1270,8 +1265,7 @@ macro_rules! device_classes { } device_classes! { - Keyboard = 0 => "keyboard", - Mouse = 1 => "mouse", + // 0 was `keyboard` and 1 `mouse`, the kernel's input streams. Framebuffer = 2 => "framebuffer", /// One PCI function, driven by whoever holds the claim: the kernel binds no /// driver to it, keeps config space, puts it in an address space of its own @@ -1282,12 +1276,7 @@ device_classes! { /// here: which functions a manifest row asks for is `toyos-manifest`'s /// grammar, resolved against the inventory. PciFunction = 7 => "pci", - /// One GPT partition, named by its unique partition GUID: the holder reads - /// and writes its blocks and no other's ([`crate::part`]). - /// - /// Like `pci`, a class whose name is not the whole of the entry — - /// `part:` — and [`DeviceRequest`] is the one parser. - Partition = 8 => "part", + // 8 was `part`, one GPT partition of a disk this kernel drove. /// One legacy ISA function, driven by whoever holds the claim: the process /// that first reads the claim may `in` and `out` the function's ports for /// the rest of its life and no other process may, and the function's lines @@ -1313,14 +1302,6 @@ pub struct PciId { } impl PciId { - /// `":"`, four lowercase hex digits each: the kernel's - /// `xhci-leave=` boot parameter. A manifest matcher's card is spelled by - /// `toyos-manifest`'s own rule. - pub fn parse(text: &str) -> Option { - let (vendor, device) = text.split_once(':')?; - Some(Self { vendor: hex16(vendor)?, device: hex16(device)? }) - } - /// The id as one word. pub const fn wire(self) -> u64 { ((self.vendor as u64) << 16) | self.device as u64 @@ -1482,8 +1463,7 @@ fn decimal_line(text: &str) -> Option { } /// What a claim asks for: a class, and for [`DeviceType::PciFunction`] which -/// function, for a partition which partition, and for an ISA function which -/// ports and lines. +/// function, and for an ISA function which ports and lines. /// /// Every request but a PCI function's is also a `devices` entry, spelled once /// here ([`Self::parse`], [`Self::write_name`]): the manifest that names it and @@ -1492,35 +1472,28 @@ fn decimal_line(text: &str) -> Option { pub enum DeviceRequest { Class(DeviceType), Function(crate::inventory::Arrival), - Partition(crate::part::PartGuid), Isa(IsaId), } impl DeviceRequest { - /// The spelling that carries a unique partition GUID. - pub const PART_PREFIX: &'static str = "part:"; - /// The spelling that carries an ISA function's ports and lines. pub const ISA_PREFIX: &'static str = "isa:"; /// The longest a `devices` entry, and so a `dev:` label's tail, can be: - /// `part:` and a GUID, rounded up to a multiple of eight. - pub const MAX_NAME: usize = 48; + /// an `isa:` entry's whole set, rounded up to a multiple of eight. + pub const MAX_NAME: usize = 40; /// The request a `devices` entry spells, or `None`; never a /// [`Self::Function`], which has no spelling. pub fn parse(name: &str) -> Option { - if let Some(guid) = name.strip_prefix(Self::PART_PREFIX) { - return crate::part::PartGuid::parse(guid).map(Self::Partition); - } if let Some(set) = name.strip_prefix(Self::ISA_PREFIX) { return IsaId::parse(set).map(Self::Isa); } let class = DeviceType::from_class_name(name)?; match class { - // A bare `pci`, `part` or `isa` names no device, and would - // otherwise leave the selector at zero. - DeviceType::PciFunction | DeviceType::Partition | DeviceType::Isa => None, + // A bare `pci` or `isa` names no device, and would otherwise leave + // the selector at zero. + DeviceType::PciFunction | DeviceType::Isa => None, _ => Some(Self::Class(class)), } } @@ -1529,7 +1502,6 @@ impl DeviceRequest { match self { Self::Class(class) => class, Self::Function(_) => DeviceType::PciFunction, - Self::Partition(_) => DeviceType::Partition, Self::Isa(_) => DeviceType::Isa, } } @@ -1540,7 +1512,6 @@ impl DeviceRequest { match self { Self::Class(_) => [0, 0], Self::Function(arrival) => [arrival.0 as u64, 0], - Self::Partition(guid) => guid.wire(), Self::Isa(set) => set.wire(), } } @@ -1550,7 +1521,6 @@ impl DeviceRequest { /// none. The name is also the tail of the `dev:` label the claim arrives /// under, and it is composed here so the two cannot drift. pub fn write_name(self, buf: &mut [u8; Self::MAX_NAME]) -> Option<&str> { - use crate::part::GUID_TEXT_LEN; Some(match self { Self::Function(_) => return None, Self::Class(class) => { @@ -1558,14 +1528,6 @@ impl DeviceRequest { buf[..name.len()].copy_from_slice(name.as_bytes()); core::str::from_utf8(&buf[..name.len()]).expect("a class name is ASCII") } - Self::Partition(guid) => { - let at = Self::PART_PREFIX.len(); - buf[..at].copy_from_slice(Self::PART_PREFIX.as_bytes()); - let mut text = [0u8; GUID_TEXT_LEN]; - guid.write_text(&mut text); - buf[at..at + GUID_TEXT_LEN].copy_from_slice(&text); - core::str::from_utf8(&buf[..at + GUID_TEXT_LEN]).expect("a prefix and a GUID's text are ASCII") - } Self::Isa(set) => { let mut at = Self::ISA_PREFIX.len(); buf[..at].copy_from_slice(Self::ISA_PREFIX.as_bytes()); @@ -1621,46 +1583,6 @@ pub fn device_claim(syscap: RawHandle, request: DeviceRequest) -> Result Result<(), SyscallError> { - check_unit(syscall( - SYS_PARTITION_READ, - claim.0 as u64, - first, - buf.as_mut_ptr() as u64, - buf.len() as u64, - )) -} - -/// Write `buf.len()` blocks to the partition `claim` holds, from its block -/// `first`, refused as [`partition_read`] is. -/// -/// `Ok` is the device having taken the blocks, and **not** their being -/// durable: that is [`fsync`] on the claim, which a writer that must survive a -/// power cut calls before it tells anyone the write happened. -pub fn partition_write( - claim: RawHandle, - first: u64, - buf: &[crate::part::Block], -) -> Result<(), SyscallError> { - check_unit(syscall( - SYS_PARTITION_WRITE, - claim.0 as u64, - first, - buf.as_ptr() as u64, - buf.len() as u64, - )) -} - /// Enter the real-time scheduling band, presenting a `SysCap` handle that /// carries [`Rights::RT`]. The privilege a device claim was never enough to /// confer. @@ -2709,15 +2631,6 @@ mod tests { modules(&answer).for_each(drop); } - /// `xhci-leave=`'s id, four lowercase hex digits each. - #[test] - fn the_xhci_leave_parameter_is_an_id_in_four_lowercase_hex_digits_each() { - assert_eq!(PciId::parse("8086:9a13"), Some(PciId { vendor: 0x8086, device: 0x9a13 })); - for bad in ["8086:9A13", "8086:9a1", "8086", "8086:9a13:"] { - assert_eq!(PciId::parse(bad), None, "{bad:?} parsed"); - } - } - /// The four readers of a `devices` entry share this parser, so what it /// accepts is what a config may say; a PCI function is named by its /// arrival, which no entry spells. @@ -2770,12 +2683,10 @@ mod tests { #[test] fn a_request_writes_back_the_name_it_parsed() { for name in [ - "mouse", - // The longest entry there is, which is what `MAX_NAME` is for. - "part:0FC63DAF-8483-4772-8E79-3D69D8477DE4", + "framebuffer", "isa:0060,0064:1,12", "isa:0001:0", - // The longest an ISA entry can be. + // The longest entry there is, which is what `MAX_NAME` is for. "isa:fff0,fff1,fff2,fffe:12,13,14,15", ] { let request = DeviceRequest::parse(name).expect("a name this table has"); @@ -2784,18 +2695,6 @@ mod tests { } } - /// A `part:` entry is the partition class carrying its GUID's sixteen bytes - /// in both selector words. - #[test] - fn a_partition_entry_carries_its_guid_in_both_selector_words() { - use crate::part::PartGuid; - let guid = PartGuid::parse("0FC63DAF-8483-4772-8E79-3D69D8477DE4").expect("a GUID"); - let request = DeviceRequest::parse("part:0FC63DAF-8483-4772-8E79-3D69D8477DE4"); - assert_eq!(request, Some(DeviceRequest::Partition(guid))); - assert_eq!(request.map(DeviceRequest::class), Some(DeviceType::Partition)); - assert_eq!(request.map(DeviceRequest::selector), Some(guid.wire())); - } - /// An `isa:` entry is its ports in the first selector word and its lines in /// the second, and the kernel decodes nothing a config could not have /// written: a port lane after a gap, or a line past the bus, is no set. diff --git a/sdk/abi/src/trace.rs b/sdk/abi/src/trace.rs index 6c66c63b73a..5268dae645f 100644 --- a/sdk/abi/src/trace.rs +++ b/sdk/abi/src/trace.rs @@ -73,9 +73,7 @@ pub enum Kind { TimerFire = 11, /// `data` timer expiries taken in Ring 0 since the last return to Ring 3. TimerFireBurst = 12, - /// An interrupt's record was consumed: `data`'s top byte is its source, - /// its low 24 bits the microseconds since the interrupt, saturating. - IrqDrain = 13, + // 13 was `IrqDrain`, the kernel's own drivers' interrupt records. /// Written only by a test kernel's flood; `data` is its index. Mark = 14, } @@ -96,7 +94,6 @@ impl Kind { 10 => Self::TimerStop, 11 => Self::TimerFire, 12 => Self::TimerFireBurst, - 13 => Self::IrqDrain, 14 => Self::Mark, _ => return None, }) diff --git a/sdk/libc/src/posix_io.rs b/sdk/libc/src/posix_io.rs index cf5420bde8a..bca2b93e4d1 100644 --- a/sdk/libc/src/posix_io.rs +++ b/sdk/libc/src/posix_io.rs @@ -181,7 +181,6 @@ unsafe fn answered(answer: Result, buf: *m syscall::FileType::Symlink => S_IFLNK | 0o777, syscall::FileType::Pipe => S_IFIFO | 0o644, syscall::FileType::Tty => S_IFCHR | 0o644, - syscall::FileType::Keyboard => S_IFCHR | 0o444, syscall::FileType::Serial => S_IFCHR | 0o644, syscall::FileType::Null => S_IFCHR | 0o666, _ => 0o644, @@ -409,7 +408,7 @@ pub unsafe extern "C" fn access(path: *const u8, _mode: i32) -> i32 { #[no_mangle] pub unsafe extern "C" fn isatty(raw_fd: i32) -> i32 { match syscall::fstat(fd(raw_fd)) { - Ok(st) => (st.file_type == syscall::FileType::Tty || st.file_type == syscall::FileType::Keyboard) as i32, + Ok(st) => (st.file_type == syscall::FileType::Tty) as i32, Err(_) => 0, } } diff --git a/sdk/libc/src/stdio.rs b/sdk/libc/src/stdio.rs index 9aa9676d34a..a4e1e14f60b 100644 --- a/sdk/libc/src/stdio.rs +++ b/sdk/libc/src/stdio.rs @@ -167,7 +167,7 @@ unsafe fn fd_is_interactive(fd: i32) -> bool { match syscall::fstat(RawHandle(fd as u32)) { Ok(st) => matches!( st.file_type, - syscall::FileType::Tty | syscall::FileType::Serial | syscall::FileType::Keyboard + syscall::FileType::Tty | syscall::FileType::Serial ), Err(_) => false, } diff --git a/sdk/std/sys/stdio.rs b/sdk/std/sys/stdio.rs index 22f91fc667f..3274c9600ca 100644 --- a/sdk/std/sys/stdio.rs +++ b/sdk/std/sys/stdio.rs @@ -128,8 +128,7 @@ impl Stdin { impl io::Read for Stdin { fn read(&mut self, buf: &mut [u8]) -> io::Result { let stat = syscall::fstat(STDIN).ok(); - let interactive = - stat.is_some_and(|s| s.file_type == FileType::Keyboard || s.file_type == FileType::Tty); + let interactive = stat.is_some_and(|s| s.file_type == FileType::Tty); if STDIN_RAW.load(Ordering::Relaxed) || !interactive { syscall::read(STDIN, buf).map_err(to_io_error) } else { diff --git a/sdk/toyos/src/device.rs b/sdk/toyos/src/device.rs index cef28b4156f..2e0bbe5acaa 100644 --- a/sdk/toyos/src/device.rs +++ b/sdk/toyos/src/device.rs @@ -21,7 +21,6 @@ macro_rules! device_info { device_info!( toyos_abi::FramebufferInfo, toyos_abi::pci::PciFunctionInfo, - toyos_abi::part::PartitionInfo, toyos_abi::acpi::AcpiInfo, ); @@ -51,41 +50,6 @@ pub(crate) fn read_info(dev: &Device) -> Result Ok(unsafe { val.assume_init() }) } -pub struct Keyboard(pub(crate) Device); - -impl Keyboard { - /// Non-blocking read of pending key events; empty surfaces as `Err(WouldBlock)`. - /// - /// Event loops must only ever read this handle non-blocking. The kernel wakes - /// keyboard watchers only when a report queued an event, so readiness and - /// "there is data" agree today — but a blocking read that loses the race - /// with another reader parks the caller until the next real key, and an - /// event loop that stops pumping is a frozen window. - pub fn read_nonblock(&self, buf: &mut [u8]) -> Result { - self.0.0.read_nonblock(buf) - } -} - -impl AsHandle for Keyboard { - fn as_handle(&self) -> RawHandle { self.0.as_handle() } -} - -pub struct Mouse(pub(crate) Device); - -impl Mouse { - /// Non-blocking read of pending mouse events; empty surfaces as `Err(WouldBlock)`. - /// - /// Same rationale as [`Keyboard::read_nonblock`]: an event loop that can - /// park on an empty queue is a frozen window. - pub fn read_nonblock(&self, buf: &mut [u8]) -> Result { - self.0.0.read_nonblock(buf) - } -} - -impl AsHandle for Mouse { - fn as_handle(&self) -> RawHandle { self.0.as_handle() } -} - pub struct FramebufferDev(pub(crate) Device); impl FramebufferDev { @@ -260,33 +224,3 @@ impl AsHandle for PciDev { fn as_handle(&self) -> RawHandle { self.0.as_handle() } } -/// One GPT partition, read and written in its own block numbers -/// ([`toyos_abi::part`]); a neighbour's blocks have no spelling here. -pub struct PartitionDev(pub(crate) Device); - -impl PartitionDev { - /// Which partition this is and how long; read once per claim. - pub fn describe(&self) -> Result { - read_info(&self.0) - } - - /// `buf.len()` blocks from the partition's block `first`. - pub fn read(&self, first: u64, buf: &mut [toyos_abi::part::Block]) -> Result<(), SyscallError> { - syscall::partition_read(self.0.as_handle(), first, buf) - } - - /// `buf.len()` blocks to the partition's block `first`; durable only once - /// [`Self::sync`] answers `Ok`. - pub fn write(&self, first: u64, buf: &[toyos_abi::part::Block]) -> Result<(), SyscallError> { - syscall::partition_write(self.0.as_handle(), first, buf) - } - - /// Every write this claim returned from, out of the device's cache. - pub fn sync(&self) -> Result<(), SyscallError> { - syscall::fsync(self.0.as_handle()) - } -} - -impl AsHandle for PartitionDev { - fn as_handle(&self) -> RawHandle { self.0.as_handle() } -} diff --git a/sdk/toyos/src/endow.rs b/sdk/toyos/src/endow.rs index f9d967eb6df..a5fce33ee03 100644 --- a/sdk/toyos/src/endow.rs +++ b/sdk/toyos/src/endow.rs @@ -70,12 +70,10 @@ from_handle! { SysCap => SysCap, Device => Device, crate::Console => crate::Console, - crate::Keyboard => |h| crate::Keyboard(Device(h)), - crate::Mouse => |h| crate::Mouse(Device(h)), + crate::Pipe => crate::Pipe, crate::FramebufferDev => |h| crate::FramebufferDev(Device(h)), crate::PciDev => |h| crate::PciDev(Device(h)), crate::AcpiDev => |h| crate::AcpiDev(Device(h)), - crate::PartitionDev => |h| crate::PartitionDev(Device(h)), } /// This process's endowment table, parsed once. @@ -235,16 +233,8 @@ pub fn claim(name: &str) -> Option { with_prefixed(DEV_PREFIX, name, |label| Endowments::get().take::(label)) } -/// The claim for one partition the manifest says this program gets, looked up -/// by the entry's own spelling, written by the one [`DeviceRequest`] that also -/// parsed it. -pub fn partition(guid: toyos_abi::part::PartGuid) -> Option { - let mut buf = [0u8; DeviceRequest::MAX_NAME]; - claim(DeviceRequest::Partition(guid).write_name(&mut buf)?) -} - /// The claim for one legacy ISA function the manifest says this program gets, -/// looked up by the entry's own spelling exactly as [`partition`] is. +/// looked up by the entry's own spelling. pub fn isa(set: toyos_abi::syscall::IsaId) -> Option { let mut buf = [0u8; DeviceRequest::MAX_NAME]; claim(DeviceRequest::Isa(set).write_name(&mut buf)?) diff --git a/sdk/toyos/src/lib.rs b/sdk/toyos/src/lib.rs index 109dcd52e26..50cc7d984e1 100644 --- a/sdk/toyos/src/lib.rs +++ b/sdk/toyos/src/lib.rs @@ -39,7 +39,7 @@ pub mod volatile; pub use ipc::Connection; pub use device::{ - AcpiDev, DmaRegion, FramebufferDev, Keyboard, Mouse, PartitionDev, PciDev, + AcpiDev, DmaRegion, FramebufferDev, PciDev, }; pub use toyos_abi::RawHandle; diff --git a/sdk/toyos/src/syscap.rs b/sdk/toyos/src/syscap.rs index c6a7238b5db..0118885a120 100644 --- a/sdk/toyos/src/syscap.rs +++ b/sdk/toyos/src/syscap.rs @@ -41,19 +41,6 @@ impl SysCap { self.mint(DeviceRequest::Function(arrival)) } - /// Mint the claim for one GPT partition, by its unique GUID. Apart from - /// [`Self::claim`] for the reason [`Self::claim_function`] is: this one - /// cannot be asked without naming which. - /// - /// `PermissionDenied` is a partition the kernel has mounted, and - /// `AlreadyExists` one another process holds: a partition has one holder. - pub fn claim_partition( - &self, - guid: toyos_abi::part::PartGuid, - ) -> Result { - self.mint(DeviceRequest::Partition(guid)) - } - /// Mint the claim for one legacy ISA function, by exactly its ports and /// lines. Apart from [`Self::claim`] for the reason [`Self::claim_function`] is. /// diff --git a/system/compositor/src/session.rs b/system/compositor/src/session.rs index 03206851fb5..aa45dd0cbeb 100644 --- a/system/compositor/src/session.rs +++ b/system/compositor/src/session.rs @@ -18,11 +18,10 @@ use toyos::ipc::{Connection, FrameRx, RxStep}; use toyos::poller::{Poller, READABLE}; use toyos::port::Acceptor; use toyos::shm::SharedMemory; -use toyos::{ipc, system, AsHandle, Keyboard, Mouse}; +use toyos::{ipc, system, AsHandle}; use toyos_abi::input::RawKeyEvent; -use toyos_abi::syscall::DeviceType; use toyos_abi::RawHandle; -use toyos_input::wire::{self, Record, CONNECTIONS, MAX_PAYLOAD, STREAM_KEY_LEN, STREAM_POINTER_LEN}; +use toyos_input::wire::{self, Record, CONNECTIONS, MAX_PAYLOAD}; use toyos_input::wire::DEVICES; use toyos_input::{Full, Merge, Pointer, Slot, State}; use toyos_desktop::clipboard::{Landing, Watch}; @@ -246,12 +245,10 @@ pub struct Session { /// whose package asks a question only once the question is answered, /// through this loop. 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. + /// Every source's keys and pointers. merge: Merge, pointers: Pointer, poller: Poller, @@ -324,10 +321,6 @@ impl Session { .expect("the manifest declares this program serves `clipboard`"); let settings = endow::acceptor(toyos_manifest::display::PORT) .expect("the manifest declares this program serves `display`"); - let kb: Keyboard = endow::device(DeviceType::Keyboard) - .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 (mut display, panel) = Display::claim(); @@ -383,8 +376,6 @@ impl Session { + VOLUME_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); poller.watch(&prompts, READABLE, prompts.as_handle().0 as u64); poller.watch(&clipboards, READABLE, clipboards.as_handle().0 as u64); @@ -416,8 +407,6 @@ impl Session { watcher: None, watch: Watch::default(), launcher, - kb, - mouse, inputs, sources: Default::default(), merge: Merge::default(), @@ -500,8 +489,6 @@ impl Session { self.poller.wait(1, timeout.as_nanos() as u64, |token| ready.push(token)); self.ready = ready; - 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()))) @@ -530,9 +517,7 @@ impl Session { } self.pending.retain(|p| now.duration_since(p.since) < HANDSHAKE_TIMEOUT); - if !kb_ready - && !mouse_ready - && !inputs_ready + if !inputs_ready && sources_ready.is_empty() && !accept_ready && !prompt_ready @@ -549,12 +534,6 @@ impl Session { if display_ready { self.display.woke(); } - if kb_ready { - self.keys(); - } - if mouse_ready { - self.pointer(); - } if inputs_ready { self.admit(); } @@ -581,7 +560,7 @@ impl Session { self.watched(); self.reap(); self.prompt_gone(); - self.rearm(kb_ready, mouse_ready, inputs_ready, accept_ready, prompt_ready, clipboard_ready, settings_ready); + self.rearm(inputs_ready, accept_ready, prompt_ready, clipboard_ready, settings_ready); true } @@ -668,24 +647,6 @@ impl Session { self.display.screen().pixel_format_raw() } - /// The kernel's keyboard stream, one source to the merge. - fn keys(&mut self) { - let mut buf = [0u8; 8 * STREAM_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::(); - assert!(torn.is_empty(), "compositor: the keyboard read {n} bytes, not whole events"); - for raw in events { - let merged = wire::key(raw).map(|key| self.merged_key(Slot::KERNEL, 0, key)); - if !matches!(merged, Ok(Ok(()))) { - eprintln!("compositor: the kernel's keyboard stream carried {raw:02x?}, refused: {merged:?}"); - } - } - } - /// `key` from `slot`'s device `source`, and whatever it changed of the /// merged keyboard. fn merged_key(&mut self, slot: Slot, source: u16, key: RawKeyEvent) -> Result<(), Full> { @@ -775,22 +736,6 @@ impl Session { } } - /// The kernel's mouse stream, one source to the merge. - fn pointer(&mut self) { - let mut buf = [0u8; 64 * STREAM_POINTER_LEN]; - let n = self.mouse.read_nonblock(&mut buf).unwrap_or(0); - 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).map(|report| self.pointers.report(Slot::KERNEL, report)) { - Ok(Ok(state)) => states.push(state), - refused => eprintln!("compositor: the kernel's mouse stream carried {raw:02x?}, refused: {refused:?}"), - } - } - self.motion(&states); - } - /// What the merged pointer did over `states`. fn motion(&mut self, states: &[State]) { let sample = fold_mouse(self.prev_buttons, states); @@ -1902,8 +1847,6 @@ impl Session { /// Re-arm the one-shot poll registrations for every handle that fired. fn rearm( &mut self, - kb: bool, - mouse: bool, inputs: bool, acceptor: bool, prompts: bool, @@ -1937,12 +1880,6 @@ impl Session { self.poller.watch(&w.conn, READABLE, handle.0 as u64); } } - if kb { - self.poller.watch(&self.kb, READABLE, self.kb.as_handle().0 as u64); - } - if mouse { - self.poller.watch(&self.mouse, READABLE, self.mouse.as_handle().0 as u64); - } if acceptor { let h = self.acceptor.as_handle(); self.poller.watch(&self.acceptor, READABLE, h.0 as u64); diff --git a/system/console/src/main.rs b/system/console/src/main.rs index 531b0dce8f7..6f25d906a63 100644 --- a/system/console/src/main.rs +++ b/system/console/src/main.rs @@ -36,10 +36,10 @@ 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::{FramebufferDev, Pipe}; use toyos_abi::input::RawKeyEvent; use toyos_abi::syscall::{DeviceType, SyscallError}; -use toyos_input::wire::{self, Record, CONNECTIONS, DEVICES, MAX_PAYLOAD, STREAM_KEY_LEN}; +use toyos_input::wire::{self, Record, CONNECTIONS, DEVICES, MAX_PAYLOAD}; use toyos_input::{Merge, Slot}; use toyos_logstream::{Lines, Showing, Source}; use window::Screen; @@ -202,11 +202,8 @@ 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. + // The `input` port and its sources, merged. 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(); @@ -220,13 +217,12 @@ fn main() { info.width, info.height ); - // The declared set: the shell's two output pipes, the keyboard, the log, + // The declared set: the shell's two output pipes, the log, // 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); + let poller = Poller::new(3 + 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; @@ -240,7 +236,6 @@ fn main() { loop { poller.watch_raw(toyos::RawHandle(shell.stdout.as_raw_fd() as u32), READABLE, TOKEN_STDOUT); poller.watch_raw(toyos::RawHandle(shell.stderr.as_raw_fd() as u32), READABLE, TOKEN_STDERR); - poller.watch(&kb, READABLE, TOKEN_KEYBOARD); poller.watch_raw(host.acceptor_handle(), READABLE, TOKEN_LISTEN); for client in host.client_handles() { poller.watch_raw(client, READABLE, TOKEN_CLIENT); @@ -388,21 +383,6 @@ fn main() { keys.extend(merge.end(slot)); } } - if ready[TOKEN_KEYBOARD as usize] { - let mut buf = [0u8; 16 * STREAM_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(&mut buf).unwrap_or(0); - for raw in buf[..n].as_chunks::().0 { - match wire::key(raw).map(|key| merge.key(Slot::KERNEL, 0, key)) { - Ok(Ok(delivered)) => keys.extend(delivered), - refused => eprintln!( - "console: the kernel's keyboard stream carried {raw:02x?}, refused: {:?}", - refused.map(|r| r.map(drop)) - ), - } - } - } 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. diff --git a/system/diskserver/src/disk.rs b/system/diskserver/src/disk.rs index 8afef8ec6a3..faf2b732348 100644 --- a/system/diskserver/src/disk.rs +++ b/system/diskserver/src/disk.rs @@ -1,6 +1,5 @@ -//! A partition as a program that reads and writes its blocks holds it: one a -//! block service serves ([`Served`]), or one the kernel's own disk serves -//! through a claim ([`Claimed`]), until usbd serves the stick. +//! A partition as a program that reads and writes its blocks holds it: one +//! the block service serves ([`Served`]). //! //! **A block service that ends is survived, not hidden**: a [`Served`] disk //! whose session ended reconnects through the same name and issues again every @@ -10,64 +9,11 @@ //! twice. use crate::{Error as SessionError, Outcome, Session}; -use toyos::PartitionDev; -use toyos_abi::part::{Block, MAX_BLOCKS_PER_CALL}; use toyos_blockring::MAX_REQUEST_BLOCKS; pub use toyos_disk::{span, Disk, DiskError, BLOCK}; -/// A partition the kernel's own disk serves: the stick, until usbd serves it. -pub struct Claimed { - claim: PartitionDev, - blocks: u64, -} - -impl Claimed { - pub fn new(claim: PartitionDev) -> Result { - let info = claim.describe().map_err(|_| DiskError::Device)?; - Ok(Self { claim, blocks: info.blocks }) - } -} - -impl Disk for Claimed { - fn blocks(&self) -> u64 { - self.blocks - } - - fn read(&mut self, first: u64, out: &mut [u8]) -> Result<(), DiskError> { - span(first, out.len(), self.blocks)?; - let mut buf: Vec = vec![[0u8; BLOCK]; MAX_BLOCKS_PER_CALL]; - for (i, chunk) in out.chunks_mut(BLOCK * MAX_BLOCKS_PER_CALL).enumerate() { - let n = chunk.len() / BLOCK; - let at = first + (i * MAX_BLOCKS_PER_CALL) as u64; - self.claim.read(at, &mut buf[..n]).map_err(|_| DiskError::Device)?; - for (dst, src) in chunk.chunks_exact_mut(BLOCK).zip(&buf[..n]) { - dst.copy_from_slice(src); - } - } - Ok(()) - } - - fn write(&mut self, first: u64, data: &[u8]) -> Result<(), DiskError> { - span(first, data.len(), self.blocks)?; - let mut buf: Vec = vec![[0u8; BLOCK]; MAX_BLOCKS_PER_CALL]; - for (i, chunk) in data.chunks(BLOCK * MAX_BLOCKS_PER_CALL).enumerate() { - let n = chunk.len() / BLOCK; - for (dst, src) in buf[..n].iter_mut().zip(chunk.chunks_exact(BLOCK)) { - dst.copy_from_slice(src); - } - let at = first + (i * MAX_BLOCKS_PER_CALL) as u64; - self.claim.write(at, &buf[..n]).map_err(|_| DiskError::Device)?; - } - Ok(()) - } - - fn flush(&mut self) -> Result<(), DiskError> { - self.claim.sync().map_err(|_| DiskError::Device) - } -} - -/// A partition diskserver serves. +/// A partition the block service serves. pub struct Served { session: Session, blocks: u64, diff --git a/system/diskserver/src/main.rs b/system/diskserver/src/main.rs index 323b7859fbc..fbe3b654769 100644 --- a/system/diskserver/src/main.rs +++ b/system/diskserver/src/main.rs @@ -112,6 +112,20 @@ impl Known { self.expected.iter().all(|d| self.fates.contains_key(d)) && !self.fates.values().any(|f| *f == Fate::Ended) } + /// Driver `badge`'s connection ended. One that had settled is awaited until + /// it settles again (`true`, so the bound counts from now). One that never + /// settled is unusable at once: its end is its last word, and every + /// question waiting on it is answered now rather than at the bound. + fn ended(&mut self, badge: Badge) -> bool { + match self.fates.insert(badge, Fate::Ended) { + Some(_) => true, + None => { + self.fates.insert(badge, Fate::Unusable); + false + } + } + } + /// The drivers whose controllers' partitions cannot be said absent: those /// expected and never heard, and those unusable or ended. fn unsettled(&self) -> Vec { @@ -429,19 +443,17 @@ impl Service { self.known.offered.extend(offer.parts.into_iter().map(|part| Offered { disk: ours, part })); } - /// Driver `id` is gone, and every disk it offered with it. One that had - /// settled is awaited until a driver with its badge settles again; one that - /// never settled is forgotten. + /// Driver `id` is gone, and every disk it offered with it ([`Known::ended`]). fn drop_driver(&mut self, id: u64, why: &str) { let d = self.drivers.remove(&id).expect("a live driver"); let label = d.label.unwrap_or_else(|| "unnamed".into()); println!("diskserver: dropping disk driver {} ({label}) — {why}; its {} disk(s) leave", d.badge, d.disks.len()); let gone: Vec = d.disks.values().copied().collect(); self.known.offered.retain(|o| !gone.contains(&o.disk)); - if self.known.fates.insert(d.badge, Fate::Ended).is_some() { + if self.known.ended(d.badge) { self.awaited_since = Instant::now(); } else { - self.known.fates.remove(&d.badge); + println!("diskserver: disk driver {} ended before it settled; its controller is unusable", d.badge); } } @@ -712,6 +724,22 @@ mod tests { assert!(known.settled()); } + /// A driver that ends before it settles holds nothing up: what was waiting + /// on it is answered at once, `Unusable` for its controller's partitions, + /// and it is awaited again only once it has settled. + #[test] + fn a_driver_that_ends_before_it_settles_is_unusable_at_once() { + let mut known = known(&[STICK, NVME]); + known.fates.insert(NVME, Fate::Offered); + assert_eq!(known.open(LOG, unique(LOG), false), None); + assert!(!known.ended(STICK), "a driver that never settled was awaited again"); + assert!(known.settled()); + assert_eq!(known.open(LOG, unique(LOG), false), Some(Err(Refusal::Unusable))); + known.fates.insert(STICK, Fate::Offered); + assert!(known.ended(STICK), "a settled driver's end is awaited"); + assert!(!known.settled()); + } + /// A controller its driver cannot use, or one with no driver, is a disk /// there and never a machine without one. #[test] diff --git a/system/fileserver/src/data.rs b/system/fileserver/src/data.rs index daeefb41895..c49fbca2ef0 100644 --- a/system/fileserver/src/data.rs +++ b/system/fileserver/src/data.rs @@ -83,34 +83,25 @@ pub enum Probed { Foreign, } -/// Where DATA's partition is, counted over both sources at once: the claims -/// the supervisor minted on the kernel's disks and the TOYOS-DATA partitions the block +/// Where DATA's partition is, from the TOYOS-DATA partitions the block /// service lists, or why it would not list them. #[derive(Debug, PartialEq, Eq)] pub enum Located { /// This machine has none: memory stands in. Nowhere, - /// The one claim the supervisor minted. - Claimed, /// The one partition the block service serves, by its unique GUID. Served([u8; 16]), /// Two or more, never guessed between, or none countable: DATA is absent. Refused(String), } -pub fn find(claims: usize, served: Result<&[[u8; 16]], String>) -> Located { - let served = match served { - Ok(served) => served, - Err(why) => return Located::Refused(why), - }; - match (claims, served) { - (0, []) => Located::Nowhere, - (1, []) => Located::Claimed, - (0, [one]) => Located::Served(*one), - (claims, served) => Located::Refused(format!( - "this machine has {} DATA partitions, {claims} on the kernel's disks and {} the block \ - service serves, and a volume is one", - claims + served.len(), +pub fn find(served: Result<&[[u8; 16]], String>) -> Located { + match served { + Err(why) => Located::Refused(why), + Ok([]) => Located::Nowhere, + Ok([one]) => Located::Served(*one), + Ok(served) => Located::Refused(format!( + "this machine has {} DATA partitions the block service serves, and a volume is one", served.len() )), } @@ -811,25 +802,17 @@ mod tests { const CREATE: OpenHow = OpenHow { create: true, create_new: false, truncate: false }; const PLAIN: OpenHow = OpenHow { create: false, create_new: false, truncate: false }; - /// Two DATA partitions are refused wherever each is, and a block service - /// that would not list what it has leaves DATA absent even beside a claim: - /// memory stands in only where both sources say there is none. + /// Two DATA partitions are refused, and a block service that would not + /// list what it has leaves DATA absent: memory stands in only where it + /// says there is none. #[test] - fn data_is_one_partition_counted_over_both_sources() { + fn data_is_one_partition_the_block_service_serves() { let (a, b) = ([1; 16], [2; 16]); - assert_eq!(find(0, Ok(&[])), Located::Nowhere); - assert_eq!(find(1, Ok(&[])), Located::Claimed); - assert_eq!(find(0, Ok(&[a])), Located::Served(a)); - let two = |claims, served: &[[u8; 16]]| match find(claims, Ok(served)) { - Located::Refused(why) => why, - other => panic!("{claims} claims and {} served located {other:?}", served.len()), - }; - assert!(two(1, &[a]).starts_with("this machine has 2 DATA partitions, 1 on the kernel's disks and 1")); - assert!(two(2, &[]).starts_with("this machine has 2 DATA partitions, 2 on the kernel's disks and 0")); - assert!(two(0, &[a, b]).starts_with("this machine has 2 DATA partitions, 0 on the kernel's disks and 2")); - for claims in [0, 1] { - assert_eq!(find(claims, Err("unlisted".into())), Located::Refused("unlisted".into())); - } + assert_eq!(find(Ok(&[])), Located::Nowhere); + assert_eq!(find(Ok(&[a])), Located::Served(a)); + let Located::Refused(why) = find(Ok(&[a, b])) else { panic!("two located one") }; + assert!(why.starts_with("this machine has 2 DATA partitions"), "{why}"); + assert_eq!(find(Err("unlisted".into())), Located::Refused("unlisted".into())); } /// **A symlink where a grant's root is, is no directory**, so a connection diff --git a/system/fileserver/src/fat.rs b/system/fileserver/src/fat.rs index 3c5a97d6b75..7b08554a747 100644 --- a/system/fileserver/src/fat.rs +++ b/system/fileserver/src/fat.rs @@ -822,6 +822,9 @@ mod tests { let whole = [&held[..], &appended[..]].concat(); written(&mut image, epochs[..synced].iter().flatten()); + // Whether a stop between the chain's growth and the size's write was + // reached, the state a reset mid-append leaves on the stick. + let mut grown_before_its_size = false; for (e, epoch) in epochs.iter().enumerate().skip(synced) { for kept in 0..1u32 << epoch.len() { let stop = format!("stopped in flush epoch {e} of {} with writes {kept:#b} of its {}", epochs.len(), epoch.len()); @@ -831,7 +834,9 @@ mod tests { // Clusters no entry reaches yet, and the last sync's entry over // the append's chain: the window between the chain and its // entry, closed once the sync is whole on the disk. - let refused: Vec<_> = check(&bytes) + let complaints = check(&bytes); + grown_before_its_size |= complaints.iter().any(|c| matches!(c, Complaint::ChainTooLong { .. })); + let refused: Vec<_> = complaints .into_iter() .filter(|c| match c { _ if synced_whole => true, @@ -856,6 +861,7 @@ mod tests { } written(&mut image, epoch); } + assert!(grown_before_its_size, "no stop fell between the chain's growth and the size's write"); } /// What the driver says of a volume that stopped answering is `Io`, which diff --git a/system/fileserver/src/main.rs b/system/fileserver/src/main.rs index 250416cf614..8a5657ccdd2 100644 --- a/system/fileserver/src/main.rs +++ b/system/fileserver/src/main.rs @@ -2,15 +2,13 @@ //! of that role as capabilities. //! //! **What it holds**: the acceptor of its role's one port, endowed by the -//! supervisor under `serve:fs:`; for its volume, a claim on each partition -//! of its role a disk the kernel drives carries, and `block` in its namespace, -//! a connector to diskserver minted for its role's partition alone — every -//! DATA partition, or the one the loader named, read-only for the boot -//! volume; and nothing else of the machine. DATA is one -//! partition counted over both, and two are refused by name, never guessed -//! between (`fileserver::data::find`). Argv is the role, and for LOG and BOOT the -//! unique GUID of the partition the loader named for it when no claim on it -//! was minted. +//! supervisor under `serve:fs:`; for its volume, `block` in its +//! namespace, a connector to the block service minted for its role's +//! partition alone — every DATA partition, or the one the loader named, +//! read-only for the boot volume; and nothing else of the machine. Two DATA +//! partitions are refused by name, never guessed between +//! (`fileserver::data::find`). Argv is the role, and for LOG and BOOT the +//! unique GUID of the partition the loader named for it. //! //! **A connection is what its grant says** (`toyos::fs::Grant`): the badge //! the supervisor minted its connector with, which the kernel stamped on it and @@ -51,7 +49,7 @@ use fileserver::absent::Absent; use fileserver::bounds::{self, Hello, Slot, MAX_HANDSHAKES, MAX_SERVED, MAX_STREAMS}; use fileserver::data::{DataVolume, Located, Probed}; use fileserver::descriptions::{self, Description, Ids, Shared}; -use diskserver::disk::{Claimed, Disk, Served}; +use diskserver::disk::{Disk, Served}; use fileserver::ram::Ram; use fileserver::fat::FatVolume; use fileserver::resolve::{self, Found, Refusal as Escape, Resolved}; @@ -68,7 +66,7 @@ use toyos::shm::SharedMemory; use toyos::volatile::Window; use toyos::Pipe; use toyos_abi::handle::Rights; -use toyos_abi::syscall::{self, SyscallError, DEV_PREFIX, MAX_BADGE, SERVE_PREFIX}; +use toyos_abi::syscall::{self, SyscallError, MAX_BADGE, SERVE_PREFIX}; /// What one turn of a stream appends at most. const STREAM_READ: usize = 64 * 1024; @@ -242,19 +240,6 @@ fn main() { .serve() } -/// The partition claims the supervisor minted: one per partition of the role on a disk -/// the kernel drives. -fn claims() -> Vec { - let prefix = format!("{DEV_PREFIX}part:"); - let labels: Vec = - Endowments::get().labels().filter(|l| l.starts_with(&prefix)).map(str::to_string).collect(); - labels.iter().map(|l| Endowments::get().take(l).expect("fileserver: the claim its label names")).collect() -} - -fn describe(claim: toyos::PartitionDev) -> Result { - Claimed::new(claim).map_err(|why| format!("the partition claim would not describe itself ({why:?})")) -} - /// A session on the partition diskserver serves under `guid`. fn served(guid: [u8; 16]) -> Option { let names = endow::namespace()?; @@ -284,53 +269,25 @@ fn data_partitions() -> Result, String> { fn open_volume(role: Role, guid: Option<&str>, roots: &[&str]) -> Box { match role { - Role::Data => { - let mut claims = claims(); - let found = fileserver::data::find(claims.len(), data_partitions().as_deref().map_err(String::clone)); - let absent = |why: String| -> Box { - println!("fileserver: {why}; DATA is absent this boot"); - Box::new(Absent::new(roots, why)) - }; - match found { - Located::Nowhere => ram(roots, "this machine has no DATA partition"), - Located::Claimed => match describe(claims.pop().expect("one claim")) { - Ok(disk) => data_on(disk, roots), - Err(why) => absent(why), - }, - Located::Served(guid) => match served(guid) { - Some(disk) => data_on(disk, roots), - None => absent("the DATA partition would not open".into()), - }, - Located::Refused(why) => absent(why), - } - } + Role::Data => match fileserver::data::find(data_partitions().as_deref().map_err(String::clone)) { + Located::Nowhere => ram(roots, "this machine has no DATA partition"), + Located::Served(guid) => match served(guid) { + Some(disk) => data_on(disk, roots), + None => absent(roots, "the DATA partition would not open".into()), + }, + Located::Refused(why) => absent(roots, why), + }, Role::Log | Role::Boot => { - let writable = role == Role::Log; - let mut claims = claims(); - let mounted = match claims.pop() { - Some(claim) => { - // The supervisor claims by the loader's GUID, which the kernel - // refuses when two partitions carry it. - assert!(claims.is_empty(), "fileserver: the supervisor handed the {role:?} server two claims"); - Ok(describe(claim).and_then(|disk| fat_on(disk, writable))) - } - // The supervisor starts this role with its partition's claim or its GUID, never neither. - None => { - let guid = guid.unwrap_or_else(|| { - panic!("fileserver: the {role:?} server was started with neither its partition's claim nor its GUID") - }); - let parsed = toyos_abi::part::PartGuid::parse(guid) - .unwrap_or_else(|| panic!("fileserver: {guid} is no partition GUID")); - served(parsed.0).map(|disk| fat_on(disk, writable)).ok_or(guid) - } - }; - match mounted { - Ok(Ok(volume)) => volume, - Ok(Err(why)) => { + // The supervisor starts this role with its partition's GUID. + let guid = guid.unwrap_or_else(|| panic!("fileserver: the {role:?} server was started without its partition's GUID")); + let parsed = toyos_gpt::Guid::parse(guid).unwrap_or_else(|| panic!("fileserver: {guid} is no partition GUID")); + match served(parsed.0).map(|disk| fat_on(disk, role == Role::Log)) { + Some(Ok(volume)) => volume, + Some(Err(why)) => { println!("fileserver: the {role:?} partition does not mount: {why}"); Box::new(Absent::new(roots, why)) } - Err(guid) => Box::new(Absent::new( + None => Box::new(Absent::new( roots, format!("the {role:?} partition {guid} is on no disk this server reaches"), )), @@ -339,6 +296,11 @@ fn open_volume(role: Role, guid: Option<&str>, roots: &[&str]) -> Box Box { + println!("fileserver: {why}; DATA is absent this boot"); + Box::new(Absent::new(roots, why)) +} + fn data_on(disk: D, roots: &[&str]) -> Box { match DataVolume::probe(disk, roots, fileserver::volume::now_nanos) { Probed::Mounted(volume) => Box::new(volume), diff --git a/system/nvme/Cargo.toml b/system/nvme/Cargo.toml index b4946fde522..fbd70892c30 100644 --- a/system/nvme/Cargo.toml +++ b/system/nvme/Cargo.toml @@ -10,6 +10,7 @@ toyos = { path = "../../sdk/toyos" } toyos-blockring = { path = "../../lib/blockring" } toyos-blockserve = { path = "../../lib/blockserve" } toyos-gpt = { path = "../../lib/gpt" } +toyos-quiesce = { path = "../../lib/quiesce" } [package.metadata.toyos.host] exempt.owns = "the NVMe controller ToyOS claims for it" diff --git a/system/nvme/src/main.rs b/system/nvme/src/main.rs index 57ac2f44fb3..38d86ee4e18 100644 --- a/system/nvme/src/main.rs +++ b/system/nvme/src/main.rs @@ -35,11 +35,16 @@ use nvme::{Controller, Done, Owner}; /// large cache is the slowest thing a healthy drive does. const COMMAND_SILENCE: Duration = Duration::from_secs(10); +/// How long the stop's normal shutdown is given: a policy, below the +/// supervisor's wait for every disk driver (`toyos_quiesce::DRIVERS_MS`). +const SHUTDOWN_BOUND: Duration = Duration::from_secs(2); + /// The one disk a controller's first namespace is. const DISK: u32 = 0; const TOKEN_IRQ: u64 = 0; const TOKEN_LINK: u64 = 1; +const TOKEN_STOP: u64 = 2; const TOKEN_SESSION: u64 = 0x1_0000; /// The controller as `toyos_gpt` reads it: its own sectors, served a 4 KiB @@ -108,13 +113,15 @@ fn main() { let dev = claim(); let info = dev.describe().unwrap_or_else(|why| panic!("nvme: the claim would not describe itself: {why:?}")); let name = format!("nvme {:02x}:{:02x}.{}", info.bus, info.dev, info.func); - let mut link = Link::new(name.clone()); + let stop = Endowments::get().take::(toyos_quiesce::DRIVER_STOP); + let mut link = Link::new(name.clone()) + .unwrap_or_else(|| panic!("{name}: started with no `{}` port: a disk driver's row receives it", toyos_blockring::disk::PORT)); let mut ctrl = match Controller::open(dev, &info) { Ok(ctrl) => ctrl, Err(why) => { println!("{name}: NOT SERVING — {why}"); link.settle(Settled::Unusable); - idle(&mut link); + idle(&mut link, stop.as_ref()); } }; println!( @@ -125,20 +132,28 @@ fn main() { ctrl.lba_bytes, ctrl.sectors ); - let parts = toyos_blockserve::table::read(&name, &mut Sectors { ctrl: &mut ctrl, block: vec![0u8; BLOCK_BYTES] }); + let read = toyos_blockserve::table::read(&mut Sectors { ctrl: &mut ctrl, block: vec![0u8; BLOCK_BYTES] }); + let parts = toyos_blockserve::table::say(&name, read); let mut disk = Sessions::new(name.clone(), parts); link.offer(DISK, disk.offered()); link.settle(Settled::Offered); - serve(&name, &mut ctrl, &mut disk, &mut link) + serve(&name, &mut ctrl, &mut disk, &mut link, stop.as_ref()) } /// A driver whose controller cannot be used: it keeps its link, so the /// service goes on saying so, and opens nothing. -fn idle(link: &mut Link) -> ! { - let poller = Poller::new(1); +fn idle(link: &mut Link, stop: Option<&toyos::Pipe>) -> ! { + let poller = Poller::new(2); loop { link.watch(&poller, TOKEN_LINK); - poller.wait(1, u64::MAX, |_| {}); + if let Some(stop) = stop { + poller.watch(stop, toyos::poller::READABLE, TOKEN_STOP); + } + let mut stopping = false; + poller.wait(1, u64::MAX, |token| stopping |= token == TOKEN_STOP); + if stopping { + std::process::exit(0); + } for handed in link.take() { toyos_abi::syscall::close(handed.region); drop(handed.conn); @@ -146,19 +161,28 @@ fn idle(link: &mut Link) -> ! { } } -fn serve(name: &str, ctrl: &mut Controller, disk: &mut Sessions, link: &mut Link) -> ! { - let poller = Poller::new(2 + MAX_SESSIONS as u32); +fn serve(name: &str, ctrl: &mut Controller, disk: &mut Sessions, link: &mut Link, stop: Option<&toyos::Pipe>) -> ! { + let poller = Poller::new(3 + MAX_SESSIONS as u32); let mut ready: Vec = Vec::new(); let mut done: Vec = Vec::new(); loop { poller.watch(ctrl.claim(), toyos::poller::READABLE, TOKEN_IRQ); link.watch(&poller, TOKEN_LINK); disk.watch(&poller, TOKEN_SESSION); + if let Some(stop) = stop { + poller.watch(stop, toyos::poller::READABLE, TOKEN_STOP); + } let timeout = ctrl .oldest() .map_or(u64::MAX, |at| COMMAND_SILENCE.saturating_sub(at.elapsed()).as_nanos() as u64); ready.clear(); poller.wait(1, timeout, |token| ready.push(token)); + // The supervisor says the machine stops, every file server synced. + if ready.contains(&TOKEN_STOP) { + let done = ctrl.shut_down(SHUTDOWN_BOUND); + println!("{name}: the machine stops: shutdown {}", if done { "complete" } else { "not complete in its bound" }); + std::process::exit(0); + } if let Err(why) = ctrl.take_interrupt() { panic!( diff --git a/system/nvme/src/nvme.rs b/system/nvme/src/nvme.rs index 21a642cc591..f3ba02cb588 100644 --- a/system/nvme/src/nvme.rs +++ b/system/nvme/src/nvme.rs @@ -46,6 +46,12 @@ const DOORBELLS: usize = 0x1000; const CC_ENABLED: u32 = 1 | (6 << 16) | (4 << 20); const CSTS_RDY: u32 = 1; const CSTS_CFS: u32 = 1 << 1; +/// `CC.SHN` and `CSTS.SHST` (NVMe Base 2.0 §3.1.4.5, §3.1.4.6): a normal +/// shutdown asked for, and its completion. +const CC_SHN: u32 = 0b11 << 14; +const CC_SHN_NORMAL: u32 = 0b01 << 14; +const CSTS_SHST: u32 = 0b11 << 2; +const CSTS_SHST_COMPLETE: u32 = 0b10 << 2; const SQE_BYTES: usize = 64; const CQE_BYTES: usize = 16; @@ -301,6 +307,22 @@ impl Controller { Ok(()) } + /// The machine stops: a normal shutdown (NVMe Base 2.0 §3.6.2), which has + /// the controller make what it holds durable and finish, answered by + /// `CSTS.SHST` within `within`. Polled: the register raises no interrupt. + pub fn shut_down(&self, within: Duration) -> bool { + let cc: u32 = self.regs.read(REG_CC); + self.regs.write::(REG_CC, (cc & !CC_SHN) | CC_SHN_NORMAL); + let start = Instant::now(); + while self.csts() & CSTS_SHST != CSTS_SHST_COMPLETE { + if start.elapsed() > within { + return false; + } + std::thread::yield_now(); + } + true + } + /// Disable, program the admin queue, and enable. fn enable(&mut self) -> Result<(), Refusal> { let cc: u32 = self.regs.read(REG_CC); diff --git a/system/supervisor/lifecycle/src/claims.rs b/system/supervisor/lifecycle/src/claims.rs index 1e96d6c268c..0600157b09c 100644 --- a/system/supervisor/lifecycle/src/claims.rs +++ b/system/supervisor/lifecycle/src/claims.rs @@ -124,7 +124,7 @@ mod tests { for missing in [Missing::Unresolved(Unresolved::Absent), Missing::Refused(SyscallError::AlreadyExists)] { assert!(goes_on_without(entry, &missing, &[]), "{missing:?}"); assert!(!goes_on_without(entry, &missing, &owed), "{missing:?}"); - assert!(goes_on_without("keyboard", &missing, &owed), "{missing:?}"); + assert!(goes_on_without("acpi", &missing, &owed), "{missing:?}"); } } } diff --git a/system/supervisor/src/main.rs b/system/supervisor/src/main.rs index b9ac44fc7a0..fb9bb10ea9a 100644 --- a/system/supervisor/src/main.rs +++ b/system/supervisor/src/main.rs @@ -643,6 +643,9 @@ struct Instance { row: &'static Program, function: Pci, child: Child, + /// A disk driver's [`toyos_quiesce::DRIVER_STOP`] pipe: closing it tells the + /// driver the machine stops. + stop: Option, } /// A service's kept acceptors, shared with what [`Grants`] mints on them. @@ -934,7 +937,7 @@ impl<'a> Supervisor<'a> { for act in self.drives.decide(&rows, &records, Instant::now()) { match act { Act::Start { row, entry, function } => match self.start_instance(row, entry, function) { - Ok(child) => self.instances.push(Instance { row, function, child }), + Ok((child, stop)) => self.instances.push(Instance { row, function, child, stop }), Err(why) => { say!("supervisor: {} was not started on PCI {}: {why}", row.name, pci_at(function.at)); refused.push(format!("{entry} at PCI {}", pci_at(function.at))); @@ -969,7 +972,7 @@ impl<'a> Supervisor<'a> { /// One `drives` process of `row`, holding `function`'s claim under /// `entry`'s spelling. - fn start_instance(&mut self, row: &'static Program, entry: PciMatch, function: Pci) -> Result { + fn start_instance(&mut self, row: &'static Program, entry: PciMatch, function: Pci) -> Result<(Child, Option), String> { if self.services.len() + self.instances.len() >= MAX_CHILDREN { return Err(format!("the supervisor watches {MAX_CHILDREN} processes already")); } @@ -980,14 +983,19 @@ impl<'a> Supervisor<'a> { let mut extra = Extra { claims: vec![(format!("{DEV_PREFIX}{name}"), claim)], ..Extra::default() }; // A disk driver is who this start says it is: its `disk` connector // carries the function's arrival, which the block service keys it by. + // And told when the machine stops, which it waits for (`stop_drivers`). + let mut stop = None; if is_disk_driver(row) { extra.ports.push((toyos_blockring::disk::PORT.to_string(), self.grants.disk(function.arrival)?)); + let (read, write) = toyos::pipe_pair().map_err(|e| format!("no pipe for its stop: {e:?}"))?; + extra.stop = Some(read); + stop = Some(write); } let launcher = (&self.minted, self.sessions.machine()); let served = Served::Keep { acceptors: &[], owed: &[] }; let output = Output::Boot(&mut self.log); start(Command::new(&row.path), row, self.system, self.syscap, served, &self.connectors, &self.grants, &[], extra, launcher, output) - .map(|(child, _)| child) + .map(|(child, _)| (child, stop)) .map_err(|e| e.to_string()) } @@ -1146,29 +1154,18 @@ impl<'a> Supervisor<'a> { /// running ROOT and volume are the ones the loader read, the table is the /// one on that ROOT's disk, and the idle slot is the one whose ROOT is not /// it — and its two partitions are reached only as - /// `toyos_update::slots::grant` admits them: claimed on a disk the kernel - /// drives, and on one the block service serves, through connectors minted - /// for each. + /// `toyos_update::slots::grant` admits them, through connectors to the + /// block service minted for each. fn slot_grant(&mut self) -> Result { use toyos_abi::inventory::Role; use toyos_update::slots::{BOOT_LABEL, ROOT_LABEL, TABLE_LABEL}; let records = inventory(self.syscap)?; let running = loaded(&records, Role::Root).ok_or("the loader named no ROOT")?; let volume = loaded(&records, Role::Slot).ok_or("the loader named no slot volume")?; - let parts: Vec<_> = records - .iter() - .filter_map(|r| match r { - Record::Partition(p) => Some(*p), - _ => None, - }) - .collect(); - if let Some(root) = parts.iter().find(|p| p.unique_guid == running) { - return claimed_slots(self.syscap, &parts, *root, volume); - } let mint = |grants: &Grants<'_>, grant: BlockGrant| { grants .partitions(grant)? - .ok_or_else(|| "the ROOT this boot runs is on no disk the kernel drives, and no block service runs".to_string()) + .ok_or_else(|| "no block service runs".to_string()) }; let kind = |guid: toyos_gpt::Guid| BlockGrant { scope: Scope::Kind(guid.0), writes: false }; let tables = mint(&self.grants, kind(toyos_gpt::Guid::TOYOS_SLOTS))?; @@ -1176,7 +1173,7 @@ impl<'a> Supervisor<'a> { let roots = mint(&self.grants, kind(toyos_gpt::Guid::TOYOS_ROOT))?; let (table, table_guid, listed) = self.files("the slot table", move || served_slots(running, tables, boots, roots))??; let Some(runs) = listed.iter().find(|p| p.unique_guid == running && p.type_guid == SLOT_KINDS.root) else { - return Err("the ROOT this boot runs is on no disk the kernel or the block service serves".into()); + return Err("the ROOT this boot runs is on no disk the block service serves".into()); }; let (idle, slot) = toyos_update::slots::grant(&table, runs, volume, &listed, SLOT_KINDS).map_err(|why| why.to_string())?; let mut ports = Vec::new(); @@ -1618,7 +1615,7 @@ impl<'a> Supervisor<'a> { /// A request on `power`: `logkeeper` flushed, then the kernel asked with the /// machine's capability. Answers only a refusal: a stop that happens takes /// the supervisor, and the requester, with it. - fn stop(&self, conn: &Connection, msg_type: u32) { + fn stop(&mut self, conn: &Connection, msg_type: u32) { let Some(how) = Stop::from_message(msg_type) else { say!("supervisor: power: dropping client {} — frame {msg_type} is no stop", conn.as_handle().0); return; @@ -1626,15 +1623,73 @@ impl<'a> Supervisor<'a> { say!("{STOPPING} ({how:?})"); self.log.flush(); self.sync_files(); + let stopped = self.stop_drivers(); let refused = match how { Stop::Reboot => self.syscap.reboot(), Stop::Shutdown => self.syscap.shutdown(), }; self.log.resume(); toyos::error!("supervisor: power: the kernel refused {how:?}: {refused:?}"); + // Their functions are owed another driver, as after any end. + if stopped { + self.drive(); + } let _ = conn.try_send_bytes(power::MSG_REFUSED, &refused.to_u64().to_le_bytes()); } + /// Tell every disk driver the machine stops, after the sync that wrote + /// through it, and wait for each to end, within + /// [`toyos_quiesce::DRIVERS_MS`]: usbd hands its devices back with a port + /// reset and leaves its controller reset. One that does not end in time is + /// named, and the machine stops without it: its function is stopped by + /// the kernel's own stop, as one whose driver died is. Answers whether any + /// was told. + fn stop_drivers(&mut self) -> bool { + let until = Instant::now() + Duration::from_millis(toyos_quiesce::DRIVERS_MS); + let mut waiting: Vec = Vec::new(); + for instance in &mut self.instances { + // The pipe's end closing is the word. + if instance.stop.take().is_some() { + waiting.push(instance.function.arrival); + } + } + let told = !waiting.is_empty(); + let poller = Poller::new(waiting.len().max(1) as u32); + loop { + let mut at = 0; + while at < self.instances.len() { + let instance = &self.instances[at]; + let arrival = instance.function.arrival; + match waiting.contains(&arrival).then(|| exit_of(&instance.child)).flatten() { + Some(exit) => { + let Instance { row, function, .. } = self.instances.swap_remove(at); + say!("supervisor: power: {} on PCI {} ended for the stop with {exit}", row.name, pci_at(function.at)); + self.drives.ended(arrival, true); + waiting.retain(|a| *a != arrival); + } + None => at += 1, + } + } + let left = until.saturating_duration_since(Instant::now()); + if waiting.is_empty() || left.is_zero() { + break; + } + for instance in self.instances.iter().filter(|i| waiting.contains(&i.function.arrival)) { + poller.watch_raw(toyos::RawHandle(instance.child.as_raw_handle()), READABLE, TOKEN_ENDED); + } + poller.wait(1, left.as_nanos() as u64, |_| {}); + } + for instance in self.instances.iter().filter(|i| waiting.contains(&i.function.arrival)) { + say!( + "supervisor: power: {} on PCI {} did not end in {} ms of being told; the machine stops without it", + instance.row.name, + pci_at(instance.function.at), + toyos_quiesce::DRIVERS_MS + ); + } + told + } + /// Have every writable file server make its volume durable: the kernel's /// stop waits for no process, so what a server holds and has not written /// is lost unless it is asked first. After `logkeeper`'s flush, so the log's @@ -2375,51 +2430,6 @@ fn resolve<'a, V>(system: &'a Manifest, path: &str, judge: impl FnOnce(Target<'_ Resolved::Package(row, verdict) } -/// The slot table's partition and the idle slot's two, claimed on a disk the -/// kernel drives: the grant a `slots` row is endowed (`toyos_update::slots`), -/// where the ROOT this boot runs, `running`, is one of `parts`, and `volume` -/// is the slot volume it was booted from. -fn claimed_slots( - syscap: &SysCap, - parts: &[toyos_abi::inventory::Partition], - running: toyos_abi::inventory::Partition, - volume: [u8; 16], -) -> Result { - use toyos_abi::inventory::Partition; - let tables: Vec<&Partition> = parts - .iter() - .filter(|p| p.device == running.device && p.type_guid == toyos_gpt::Guid::TOYOS_SLOTS.0) - .collect(); - let [table_part] = tables[..] else { - return Err(format!("the machine has {} slot tables on the running ROOT's disk, and a grant needs one", tables.len())); - }; - let claim = |guid: [u8; 16], what: &str| { - syscap - .claim_partition::(toyos_abi::part::PartGuid(guid)) - .map_err(|e| refused(&format!("the {what}"), e)) - }; - let table_claim = claim(table_part.unique_guid, "slot table")?; - let (table, _) = - toyos_update::slots::read(|copies| toyos_abi::syscall::partition_read(table_claim.as_handle(), 0, copies)) - .map_err(|why| why.to_string())?; - let listed = |p: &Partition| toyos_update::slots::Listed { - device: p.device, - type_guid: p.type_guid, - unique_guid: p.unique_guid, - }; - let all: Vec<_> = parts.iter().map(listed).collect(); - let (idle, slot) = toyos_update::slots::grant(&table, &listed(&running), volume, &all, SLOT_KINDS).map_err(|why| why.to_string())?; - let boot = claim(slot.boot, "idle slot's volume")?; - let root = claim(slot.root, "idle slot's ROOT")?; - say!("supervisor: the idle slot is {}, granted with the slot table", idle.letter()); - let claims = vec![ - (toyos_update::slots::TABLE_LABEL.to_string(), table_claim), - (toyos_update::slots::BOOT_LABEL.to_string(), boot), - (toyos_update::slots::ROOT_LABEL.to_string(), root), - ]; - Ok(Extra { claims, ..Extra::default() }) -} - /// The types a slot's two partitions carry. const SLOT_KINDS: toyos_update::slots::Kinds = toyos_update::slots::Kinds { boot: toyos_gpt::Guid::TOYOS_BOOT.0, root: toyos_gpt::Guid::TOYOS_ROOT.0 }; @@ -2487,14 +2497,13 @@ fn refused(name: &str, why: SyscallError) -> String { match why { SyscallError::NotFound => format!("no {name} on this machine"), SyscallError::AlreadyExists => format!("{name} is already claimed"), - SyscallError::InvalidArgument => format!("{name} names two partitions on this machine"), SyscallError::PermissionDenied => { format!("{name} is driven by the kernel and cannot be claimed") } SyscallError::ResourceExhausted => format!("this machine has no interrupt left for {name}"), SyscallError::NotSupported => { format!("{name} is on this machine and could not be handed over; the kernel's \ - `pcidev:`, `partclaim:`, `isa:` or `acpi:` line says why") + `pcidev:`, `isa:` or `acpi:` line says why") } other => format!("{name} was refused with {other:?}"), } @@ -2584,7 +2593,7 @@ fn start<'a>( launcher: (&Minted, Session), output: Output<'_>, ) -> Result<(Child, Vec), StartError> { - let Extra { args, claims, ports } = extra; + let Extra { args, claims, ports, stop } = extra; // A storage row's own arguments first: a file server's role leads its argv. command.args(&args); command.args(&program.args); @@ -2611,6 +2620,11 @@ fn start<'a>( command.endow(&label, raw.0); held.0.push(raw); } + if let Some(stop) = stop { + let raw = stop.into_raw(); + command.endow(toyos_quiesce::DRIVER_STOP, raw.0); + held.0.push(raw); + } let mut view = grants.view(program, launcher.1); view.extend(ports); view.extend(grants.netstack(program, launcher.1)); @@ -3105,15 +3119,17 @@ fn swapped(service: &str, word: Word, detail: &str) { say!("{}", toyos_swap::said(service, word, detail)); } -/// What a process is started with beside its row: arguments, claims endowed -/// by label — on the partitions a disk the kernel drives carries, or a -/// `drives` process's function — and connectors in its namespace by name to -/// the partitions the block service serves, each minted for what it reaches. +/// What a process is started with beside its row: arguments, a `drives` +/// process's claim on its function endowed by label, and connectors in its +/// namespace by name, each minted for what it reaches: the partitions the +/// block service serves, or a disk driver's `disk`. #[derive(Default)] struct Extra { args: Vec, claims: Vec<(String, toyos::Device)>, ports: Vec<(String, Connector)>, + /// A disk driver's end of its [`toyos_quiesce::DRIVER_STOP`] pipe. + stop: Option, } /// Every record the kernel's inventory answers. @@ -3134,7 +3150,6 @@ fn claim(syscap: &SysCap, request: DeviceRequest) -> Result syscap.claim(class), DeviceRequest::Function(arrival) => syscap.claim_function(arrival), - DeviceRequest::Partition(guid) => syscap.claim_partition(guid), DeviceRequest::Isa(set) => syscap.claim_isa(set), } } @@ -3148,71 +3163,39 @@ fn loaded(records: &[toyos_abi::inventory::Record], role: toyos_abi::inventory:: } fn guid_text(guid: [u8; 16]) -> String { - let mut buf = [0u8; toyos_abi::part::GUID_TEXT_LEN]; - toyos_abi::part::PartGuid(guid).write_text(&mut buf).to_string() + toyos_gpt::Guid(guid).to_string() } -/// A file server's arguments, claims and block connector for one start. +/// A file server's arguments and block connector for one start. /// -/// A file server is told its role, and gets a claim on every partition of its -/// role a disk the kernel drives carries — the stick, until usbd serves it — -/// and otherwise the partition's GUID and `block` minted for -/// that partition alone, writing for the log and reading for the boot volume; -/// DATA it finds by type, through `block` minted for every DATA partition, -/// and counts with its claims. A log or boot role the loader named no -/// partition for, a claim the kernel refuses, and a block port closed for -/// good each refuse the start: the role is then absent, never served from -/// memory. +/// A file server is told its role, and for LOG and BOOT the partition's GUID +/// the loader named, with `block` minted for that partition alone, writing for +/// the log and reading for the boot volume; DATA it finds by type, through +/// `block` minted for every DATA partition. A log or boot role the loader +/// named no partition for, and a block port closed for good, each refuse the +/// start: the role is then absent, never served from memory. fn storage_endowment(role: Option<&str>, syscap: &SysCap, grants: &Grants<'_>) -> Result { - use toyos_abi::inventory::{Record, Role}; + use toyos_abi::inventory::Role; let mut storage = Extra::default(); let Some(role) = role else { return Ok(storage); }; - let records = inventory(syscap).map_err(|why| StartError::Other(std::io::Error::other(why)))?; storage.args.push(role.to_string()); - let on_kernel_disk = |pick: &dyn Fn(&toyos_abi::inventory::Partition) -> bool| -> Vec<[u8; 16]> { - records - .iter() - .filter_map(|r| match r { - Record::Partition(p) if pick(p) => Some(p.unique_guid), - _ => None, - }) - .collect() - }; - let (kernel, named, served) = match role { - "data" => { - let kind = toyos_gpt::Guid::TOYOS_DATA.0; - (on_kernel_disk(&|p| p.type_guid == kind), None, BlockGrant { scope: Scope::Kind(kind), writes: true }) - } + let served = match role { + "data" => BlockGrant { scope: Scope::Kind(toyos_gpt::Guid::TOYOS_DATA.0), writes: true }, "log" | "boot" => { + let records = inventory(syscap).map_err(|why| StartError::Other(std::io::Error::other(why)))?; let which = if role == "log" { Role::Log } else { Role::Boot }; let Some(guid) = loaded(&records, which) else { return Err(StartError::Refused(format!("the loader named no `{role}` partition"))); }; - let grant = BlockGrant { scope: Scope::Unique(guid), writes: role == "log" }; - (on_kernel_disk(&|p| p.unique_guid == guid), Some(guid), grant) + storage.args.push(guid_text(guid)); + BlockGrant { scope: Scope::Unique(guid), writes: role == "log" } } other => panic!("supervisor: `{other}` is no role; the build refuses it"), }; - for guid in &kernel { - let request = toyos_abi::syscall::DeviceRequest::Partition(toyos_abi::part::PartGuid(*guid)); - let mut buf = [0u8; toyos_abi::syscall::DeviceRequest::MAX_NAME]; - let name = request.write_name(&mut buf).expect("a partition has a name").to_string(); - match syscap.claim_partition::(toyos_abi::part::PartGuid(*guid)) { - Ok(claim) => storage.claims.push((format!("{DEV_PREFIX}{name}"), claim)), - Err(e) => return Err(StartError::Refused(refused(&name, e))), - } - } - // DATA is counted over both; a named partition no claim reached is the - // block service's. - if named.is_none() || kernel.is_empty() { - if let Some(guid) = named { - storage.args.push(guid_text(guid)); - } - if let Some(port) = grants.partitions(served).map_err(StartError::Refused)? { - storage.ports.push((toyos_blockring::PORT.to_string(), port)); - } + if let Some(port) = grants.partitions(served).map_err(StartError::Refused)? { + storage.ports.push((toyos_blockring::PORT.to_string(), port)); } Ok(storage) } diff --git a/system/update/src/main.rs b/system/update/src/main.rs index 2ada13e1e45..1f1f908720b 100644 --- a/system/update/src/main.rs +++ b/system/update/src/main.rs @@ -8,9 +8,8 @@ //! //! **What it holds is the whole of what it can write** (`slots` in its //! `system.toml` row): the slot table's partition and the idle slot's FAT -//! volume and ROOT, as claims the supervisor endows under their labels on a -//! disk the kernel drives, or as connectors in its namespace under the same -//! names, each minted for its one partition on the block service's port; the +//! volume and ROOT, as connectors in its namespace under their labels, each +//! minted for its one partition on the block service's port; the //! slot this boot runs is never among them (`toyos_update::slots::idle`). What //! it writes there, and in what order, is [`install`]'s. @@ -18,9 +17,8 @@ mod install; use std::time::Instant; -use diskserver::disk::{Claimed, Disk, Served}; -use toyos::endow::{self, Endowments}; -use toyos::PartitionDev; +use diskserver::disk::{Disk, Served}; +use toyos::endow; use toyos_update::sig; use toyos_update::slots; use install::Partition; @@ -48,15 +46,10 @@ struct Held { unique: [u8; 16], } -/// The partition endowed under `label`: a claim, or a session through the -/// connector of that name, which reaches one partition and lists it. +/// The partition endowed under `label`: a session through the connector of +/// that name, which reaches one partition and lists it. fn held(label: &str) -> Result { let none = || format!("this process holds no `{label}`: the supervisor grants the idle slot to one update at a time, and says why where it grants none"); - if let Some(claim) = Endowments::get().take::(label) { - let unique = claim.describe().map_err(|e| format!("the `{label}` claim: {e:?}"))?.unique_guid; - let disk = Claimed::new(claim).map_err(|e| format!("the `{label}` claim: {e:?}"))?; - return Ok(Held { disk: Box::new(disk), unique }); - } let names = endow::namespace().ok_or_else(none)?; let own = toyos::namespace::build().keep(names, &[label]).finish().map_err(|_| none())?; let listed = diskserver::list(&own, label).map_err(|why| format!("`{label}` would not list its partition: {why:?}"))?; diff --git a/system/usbd/Cargo.toml b/system/usbd/Cargo.toml index ec60ef1af47..59f44516e74 100644 --- a/system/usbd/Cargo.toml +++ b/system/usbd/Cargo.toml @@ -1,14 +1,19 @@ [package] name = "usbd" -description = "One xHCI controller driven from userland: the handoff, its reset and rings, its interrupt through the claim, and every root-hub device enumerated through toyos-xhci and named over inspect." +description = "One xHCI controller driven from userland: its sticks offered to the block service and served, its keyboards and pointers reported to the owner of input, and every device named over inspect." edition = "2024" license = "MIT OR Apache-2.0" [dependencies] toyos-abi = { path = "../../sdk/abi" } toyos = { path = "../../sdk/toyos" } -toyos-xhci = { path = "../../lib/xhci" } +toyos-xhci = { path = "xhci" } toyos-inspect = { path = "../../lib/inspect" } +toyos-pci-claim = { path = "../../lib/pci-claim" } +toyos-blockring = { path = "../../lib/blockring" } +toyos-blockserve = { path = "../../lib/blockserve" } +toyos-input = { path = "../../lib/input" } +toyos-quiesce = { path = "../../lib/quiesce" } [package.metadata.toyos.host] exempt.owns = "the xHCI controller ToyOS claims for it" diff --git a/system/usbd/src/bus.rs b/system/usbd/src/bus.rs deleted file mode 100644 index 1fd906f5d74..00000000000 --- a/system/usbd/src/bus.rs +++ /dev/null @@ -1,493 +0,0 @@ -//! The root hub and what is on it: every port stepped through -//! `toyos_xhci::port`, every device enumerated in `toyos_xhci::enumerate`'s -//! order, and the one operation the controller owes an answer for -//! (`toyos_xhci::job::Outstanding`). -//! -//! **Nothing here waits.** An act is submitted, its answer arrives with an -//! interrupt and is matched by the TRB it names, and the act after it is -//! submitted then; a port's debounce and reset deadlines and an operation's -//! deadline are instants handed back to the loop as its next wake. -//! -//! **A device is named and stops there**: enumeration runs to the -//! configuration descriptor and no further, so every device keeps its slot -//! and its address and no class is bound; which interface is served is a -//! class driver's, and there is none yet. -//! -//! **Every event is announced.** The event ring is read only on an interrupt -//! record, so an operation whose deadline passes with its answer on the ring -//! is an interrupt that never came, counted and named rather than taken. - -use std::num::NonZeroU8; - -use toyos_abi::inventory::UsbSpeed; -use toyos_xhci::descriptor::{self, Class, Interfaces}; -use toyos_xhci::enumerate::{self, Act, Command, Enumeration, Learnt, Next, Request}; -use toyos_xhci::job::{Await, Outcome, Outstanding, Stages, CC_SUCCESS}; -use toyos_xhci::port::{self, Gone, Nanos, PortState, Reset, Step}; - -use crate::hc::{self, Controller, Event, Ring, Trb}; - -/// How long the controller has to answer one command or transfer. Policy: -/// the kernel driver's bound for the same operations. -const ANSWER_DEADLINE: Nanos = 2_000_000_000; - -/// The steps one look at one port may take before it is called stuck: the -/// longest legitimate run is an acknowledge, a teardown and a debounce. -const STEP_BUDGET: usize = 16; - -/// What a configuration descriptor is read with: more than any device in -/// reach answers, and a longer one is walked as the prefix that arrived. -const CONFIG_BYTES: u16 = 1024; - -/// A device this controller has enumerated. -pub struct Named { - pub port: u8, - pub slot: u8, - pub speed: UsbSpeed, - pub device: descriptor::Device, - pub interfaces: Interfaces, -} - -impl Named { - /// What the device is: its own class, or its first interface's where it - /// defers to its interfaces. - pub fn class(&self) -> Class { - match self.device.class.class { - 0 => self.interfaces.iter().next().unwrap_or(self.device.class), - _ => self.device.class, - } - } -} - -/// What the loop counted, which `inspect` reads. -#[derive(Default)] -pub struct Counts { - /// Interrupt records read off the claim, and the interrupts they counted. - pub records: u64, - pub interrupts: u64, - /// Events taken off the ring, each after a record. - pub events: u64, - /// Of them, events of a type nothing this driver does raises. - pub other: u64, - /// Answers found on the ring by an operation's deadline: an event no - /// interrupt announced. - pub unannounced: u64, - /// Operations nothing answered. - pub silent: u64, - /// The bring-up's No-Op: `None` while it is outstanding. - pub noop: Option, -} - -/// What the outstanding operation is for. -enum What { - NoOp, - Enumerating(Enumerating), - /// Disable Slot; then the port is empty, or keeps a device it refused. - SlotGone { port: u8, slot: u8, refused: bool }, -} - -/// One device's enumeration, between its acts. -struct Enumerating { - port: u8, - speed: u8, - /// EP0's packet size as the controller was last told it. - packet: u16, - /// Zero until Enable Slot has answered. - slot: u8, - ep0: Option, - seq: Enumeration, - act: Act, - device: Option, - interfaces: Option, -} - -/// What a port's step asks for, with the port machine's borrow over. -enum Look { - Again, - Done(Option), - Enumerate(Option), - Teardown(Gone), -} - -pub struct Bus { - pub ctrl: Controller, - ports: Vec, - outstanding: Outstanding, - pub devices: Vec, - /// A Port Status Change arrived, or a look was cut short by an operation. - ports_dirty: bool, - port_wake: Option, - pub counts: Counts, -} - -impl Bus { - /// The ports as the controller came up, every connected one given the - /// reset a device left by whatever ran before needs - /// (`PortState::adopt`), and the No-Op that says the command ring and the - /// interrupt both work before any device depends on them. - pub fn new(mut ctrl: Controller, now: Nanos) -> Self { - let mut ports: Vec = (0..ctrl.max_ports) - .map(|p| { - let mut port = PortState::EMPTY; - port.speaks(ctrl.protocols.of(p)); - port - }) - .collect(); - for (p, port) in ports.iter_mut().enumerate() { - let p = p as u8; - let portsc = ctrl.portsc(p); - if portsc.connected() { - let (kind, writes) = port.adopt(portsc, now); - println!("usbd: port {} found connected (PORTSC {:#010x}); {kind:?} resetting it", p + 1, portsc.raw()); - for write in writes { - ctrl.write_portsc(p, write); - } - } - } - let mut outstanding = Outstanding::EMPTY; - let trb = ctrl.command(Trb { control: hc::TRB_NO_OP_COMMAND, ..Trb::default() }); - outstanding.submit(What::NoOp, Await::Command { trb }, Stages::One, now + ANSWER_DEADLINE); - Self { ctrl, ports, outstanding, devices: Vec::new(), ports_dirty: true, port_wake: None, counts: Counts::default() } - } - - /// An interrupt record of `count`: take every event the ring holds, up to - /// one ring's worth — the rest is the next record's. - pub fn interrupt(&mut self, count: u64) { - self.counts.records += 1; - self.counts.interrupts += count; - for _ in 0..hc::PAGE / 16 { - let Some(event) = self.ctrl.next_event() else { break }; - self.counts.events += 1; - match event { - Event::Command { trb, code, slot } => { - self.outstanding.answered(Await::Command { trb }, code, u32::from(slot)); - } - Event::Transfer { trb, slot, dci, code, residue } => { - self.outstanding.answered(Await::Transfer { slot, dci, trb }, code, residue); - } - // The register says what a port is; the event is a reason to look. - Event::Port => self.ports_dirty = true, - Event::Other => self.counts.other += 1, - } - } - self.ctrl.consumed(); - } - - /// Act on whatever is over, step the ports where there is reason to, and - /// answer when the loop must come back with nothing else to wake it. - pub fn pass(&mut self, now: Nanos) -> Option { - if let Some((what, outcome)) = self.outstanding.finished(now) { - if outcome == Outcome::Silent { - self.counts.silent += 1; - if self.ctrl.event_waiting() { - self.counts.unannounced += 1; - println!("usbd: an answer is on the event ring and no interrupt said so"); - } - } - self.finished(what, outcome, now); - } - if !self.outstanding.busy() && port::due(self.ports_dirty, &self.ports) { - self.ports_dirty = false; - self.port_wake = None; - for p in 0..self.ctrl.max_ports { - self.look(p, now); - } - } - match (self.outstanding.wake_at(), self.port_wake) { - (Some(a), Some(b)) => Some(a.min(b)), - (at, None) | (None, at) => at, - } - } - - /// Step one port until it rests, waits, or starts an effect. - fn look(&mut self, p: u8, now: Nanos) { - for _ in 0..STEP_BUDGET { - // An effect another port started holds the one operation slot, - // and this port's next step may need it. - if self.outstanding.busy() { - self.ports_dirty = true; - return; - } - let portsc = self.ctrl.portsc(p); - let look = match self.ports[usize::from(p)].step(portsc, now) { - Step::Idle => Look::Done(None), - Step::Wait(at) => Look::Done(Some(at)), - Step::Write(write) => { - self.ctrl.write_portsc(p, write); - Look::Again - } - Step::Reset(kind, write) => { - println!("usbd: port {} {kind:?} reset", p + 1); - self.ctrl.write_portsc(p, write); - Look::Again - } - Step::GaveUp(why) => { - println!("usbd: port {} given up on: {why:?} (PORTSC {:#010x})", p + 1, portsc.raw()); - Look::Done(None) - } - Step::Enumerate { after, pending } => { - pending.running(); - Look::Enumerate(after) - } - Step::Teardown(gone, pending) => { - pending.running(); - Look::Teardown(gone) - } - }; - match look { - Look::Again => continue, - Look::Done(at) => { - self.port_wake = match (self.port_wake, at) { - (Some(a), Some(b)) => Some(a.min(b)), - (w, None) | (None, w) => w, - }; - return; - } - Look::Enumerate(after) => return self.begin(p, after, now), - Look::Teardown(gone) => return self.teardown(p, gone, now), - } - } - println!("usbd: port {} took {STEP_BUDGET} steps and did not settle; looking again", p + 1); - self.ports_dirty = true; - } - - fn teardown(&mut self, p: u8, gone: Gone, now: Nanos) { - println!("usbd: port {}: its device is gone ({gone:?}, PORTSC {:#010x})", p + 1, self.ctrl.portsc(p).raw()); - self.devices.retain(|d| d.port != p); - match self.ports[usize::from(p)].take_slot() { - Some(slot) => self.disable(p, slot.get(), false, now), - None => self.ports[usize::from(p)].torn_down(), - } - } - - fn disable(&mut self, port: u8, slot: u8, refused: bool, now: Nanos) { - let trb = self.ctrl.command(Trb { control: hc::TRB_DISABLE_SLOT | (u32::from(slot) << 24), ..Trb::default() }); - self.outstanding.submit(What::SlotGone { port, slot, refused }, Await::Command { trb }, Stages::One, now + ANSWER_DEADLINE); - } - - /// A port whose reset finished: acknowledge it, and ask for a slot. - fn begin(&mut self, p: u8, after: Option, now: Nanos) { - let portsc = self.ctrl.portsc(p); - println!("usbd: port {} enumerating after {after:?} (PORTSC {:#010x})", p + 1, portsc.raw()); - self.ctrl.write_portsc(p, port::enumeration_ack(after, portsc)); - let portsc = self.ctrl.portsc(p); - if !portsc.enabled() { - println!("usbd: port {} reset and not enabled (PORTSC {:#010x})", p + 1, portsc.raw()); - return self.ports[usize::from(p)].enumerated(None); - } - let speed = portsc.speed(); - let (Some(packet), Some(_)) = (enumerate::initial_ep0_packet(speed), UsbSpeed::from_psiv(speed)) else { - println!("usbd: port {} came up at speed {speed}, which this driver has no default for", p + 1); - return self.ports[usize::from(p)].enumerated(None); - }; - let (seq, act) = Enumeration::begin(); - let trb = self.ctrl.command(Trb { control: hc::TRB_ENABLE_SLOT, ..Trb::default() }); - let state = Enumerating { port: p, speed, packet, slot: 0, ep0: None, seq, act, device: None, interfaces: None }; - self.outstanding.submit(What::Enumerating(state), Await::Command { trb }, Stages::One, now + ANSWER_DEADLINE); - } - - fn finished(&mut self, what: What, outcome: Outcome, now: Nanos) { - match what { - What::NoOp => { - let done = outcome.succeeded(); - self.counts.noop = Some(done); - if done { - println!("usbd: a No-Op command completed, announced by an interrupt"); - } else { - println!("usbd: the bring-up's No-Op command: {outcome:?}"); - } - } - What::SlotGone { port, slot, refused } => { - if !outcome.succeeded() { - println!("usbd: Disable Slot {slot}: {outcome:?}"); - } - self.ctrl.unbind_slot(slot); - match refused { - true => self.ports[usize::from(port)].enumerated(None), - false => self.ports[usize::from(port)].torn_down(), - } - } - What::Enumerating(state) => self.stepped(state, outcome, now), - } - } - - /// The outstanding act of an enumeration answered. - fn stepped(&mut self, mut state: Enumerating, outcome: Outcome, now: Nanos) { - let learnt = match state.act { - Act::Command(Command::EnableSlot) => { - let Outcome::Command { code: CC_SUCCESS, slot } = outcome else { - return self.refuse(state, format!("Enable Slot: {outcome:?}"), now); - }; - state.slot = slot; - let Some(block) = self.ctrl.device_block(slot) else { - return self.refuse(state, format!("slot {slot} is past the room this driver gave devices"), now); - }; - state.ep0 = Some(self.ctrl.bind_slot(block, slot)); - Learnt::Nothing - } - Act::Command(_) if outcome.succeeded() => Learnt::Nothing, - Act::Command(command) => return self.refuse(state, format!("{command:?}: {outcome:?}"), now), - Act::Request(request) => match read_back(&self.ctrl, &mut state, request, outcome) { - Ok(learnt) => learnt, - Err(why) => return self.refuse(state, why, now), - }, - }; - match state.seq.completed(learnt) { - // Where a class driver would take over: the device is named here. - Next::Act(_, Act::Request(Request::SetConfiguration)) | Next::Refuse | Next::Bind => self.named(state), - Next::Act(seq, act) => { - state.seq = seq; - state.act = act; - self.perform(state, now); - } - } - } - - fn perform(&mut self, mut state: Enumerating, now: Nanos) { - let deadline = now + ANSWER_DEADLINE; - match state.act { - Act::Command(command) => { - self.ctrl.clear_input_ctx(); - let ep0_dw1 = (3u32 << 1) | (4u32 << 3) | (u32::from(state.packet) << 16); - let kind = match command { - Command::AddressDevice => { - // Add the slot and EP0 (§4.3.3); the slot context - // names one entry, the speed and the root-hub port. - self.ctrl.input_ctx(0, 1, 0b11); - self.ctrl.input_ctx(1, 0, (u32::from(state.speed) << 20) | (1 << 27)); - self.ctrl.input_ctx(1, 1, (u32::from(state.port) + 1) << 16); - let dequeue = state.ep0.as_ref().expect("EP0 is bound with the slot").dequeue(); - self.ctrl.input_ctx(2, 1, ep0_dw1); - self.ctrl.input_ctx(2, 2, dequeue as u32); - self.ctrl.input_ctx(2, 3, (dequeue >> 32) as u32); - self.ctrl.input_ctx(2, 4, 8); - hc::TRB_ADDRESS_DEVICE - } - // EP0's packet size, and nothing else (§4.6.7). - Command::EvaluateEp0 => { - self.ctrl.input_ctx(0, 1, 1 << 1); - self.ctrl.input_ctx(2, 1, ep0_dw1); - hc::TRB_EVALUATE_CONTEXT - } - Command::EnableSlot | Command::ConfigureEndpoint | Command::ResetEp0 | Command::SetEp0Dequeue => { - unreachable!("an enumeration that stops at its configuration asks no {command:?}") - } - }; - let (_, input) = self.ctrl.grant(hc::OFF_INPUT_CTX, hc::PAGE); - let trb = self.ctrl.command(Trb { param: input, control: kind | (u32::from(state.slot) << 24), ..Trb::default() }); - self.outstanding.submit(What::Enumerating(state), Await::Command { trb }, Stages::One, deadline); - } - Act::Request(request) => { - let (w_value, want): (u16, u16) = match request { - Request::DeviceDescriptor { want } => (0x0100, want), - Request::ConfigDescriptor => (0x0200, CONFIG_BYTES), - Request::SetConfiguration | Request::SetProtocol => { - unreachable!("an enumeration that stops at its configuration sends no {request:?}") - } - }; - let (data, at) = self.ctrl.grant(hc::OFF_DATA, hc::PAGE); - data.zero(); - let ring = state.ep0.as_mut().expect("EP0 is bound with the slot"); - // GET_DESCRIPTOR, device to host (USB 2.0 §9.4.3), in three stages. - let setup = 0x80u64 | (6 << 8) | (u64::from(w_value) << 16) | (u64::from(want) << 48); - ring.enqueue(Trb { param: setup, status: 8, control: hc::TRB_SETUP_STAGE | hc::TRB_IDT | (3 << 16) }); - let data_trb = ring.enqueue(Trb { - param: at, - status: u32::from(want), - control: hc::TRB_DATA_STAGE | (1 << 16) | hc::TRB_ISP | hc::TRB_IOC, - }); - let status_trb = ring.enqueue(Trb { control: hc::TRB_STATUS_STAGE | hc::TRB_IOC, ..Trb::default() }); - let on = |trb| Await::Transfer { slot: state.slot, dci: 1, trb }; - let (first, then) = (on(data_trb), on(status_trb)); - self.ctrl.doorbell(state.slot, 1); - self.outstanding.submit(What::Enumerating(state), first, Stages::DataThenStatus(then), deadline); - } - } - } - - fn named(&mut self, state: Enumerating) { - let (Some(device), Some(interfaces)) = (state.device, state.interfaces) else { - unreachable!("a device is named only once its descriptors were read") - }; - let speed = UsbSpeed::from_psiv(state.speed).expect("checked when the port came up"); - let named = Named { port: state.port, slot: state.slot, speed, device, interfaces }; - let class = named.class(); - println!( - "usbd: port {}: {:04x}:{:04x} at {} speed, class {:02x}:{:02x}:{:02x}, on slot {}", - state.port + 1, - device.vendor, - device.product, - speed_name(speed), - class.class, - class.subclass, - class.protocol, - state.slot - ); - self.ports[usize::from(state.port)].enumerated(NonZeroU8::new(state.slot)); - self.devices.push(named); - } - - /// An enumeration that cannot go on: its slot, if it had one, goes back, - /// and the port keeps its device without one until it is pulled. - fn refuse(&mut self, state: Enumerating, why: String, now: Nanos) { - println!("usbd: port {}: {why}; leaving it unnamed", state.port + 1); - match state.slot { - 0 => self.ports[usize::from(state.port)].enumerated(None), - slot => self.disable(state.port, slot, true, now), - } - } -} - -/// What a completed GET_DESCRIPTOR left in the data page, as the order of -/// what is left depends on it. -fn read_back(ctrl: &Controller, state: &mut Enumerating, request: Request, outcome: Outcome) -> Result { - let Outcome::Transfer { code: CC_SUCCESS, residue } = outcome else { - return Err(format!("{request:?}: {outcome:?}")); - }; - let want = match request { - Request::DeviceDescriptor { want } => want, - _ => CONFIG_BYTES, - }; - // A residue past what was asked is the controller contradicting itself: - // nothing moved. - let moved = usize::from(want).saturating_sub(residue as usize); - let (page, _) = ctrl.grant(hc::OFF_DATA, hc::PAGE); - let bytes: Vec = (0..moved).map(|at| page.read::(at)).collect(); - let refused = |why: descriptor::Refused| format!("{request:?}: {why}"); - match request { - Request::DeviceDescriptor { want: 8 } => { - let stated = descriptor::ep0_packet(&bytes).map_err(refused)?; - let packet = enumerate::ep0_packet_from_descriptor(state.speed, stated) - .ok_or_else(|| format!("bMaxPacketSize0={stated} is no size a speed-{} device has", state.speed))?; - if packet == state.packet { - return Ok(Learnt::Nothing); - } - state.packet = packet; - Ok(Learnt::Ep0PacketWrong) - } - Request::DeviceDescriptor { .. } => { - state.device = Some(descriptor::device(&bytes).map_err(refused)?); - Ok(Learnt::Nothing) - } - Request::ConfigDescriptor => { - let interfaces = descriptor::interfaces(&bytes).map_err(refused)?; - state.interfaces = Some(interfaces); - // No function is learnt: which class a device's interfaces bind - // is the class driver's stage to decide. - Ok(Learnt::Nothing) - } - Request::SetConfiguration | Request::SetProtocol => unreachable!("never sent"), - } -} - -pub fn speed_name(speed: UsbSpeed) -> &'static str { - match speed { - UsbSpeed::Low => "low", - UsbSpeed::Full => "full", - UsbSpeed::High => "high", - UsbSpeed::Super => "super", - UsbSpeed::SuperPlusGen2x1 => "super-plus-gen2x1", - UsbSpeed::SuperPlusGen1x2 => "super-plus-gen1x2", - UsbSpeed::SuperPlusGen2x2 => "super-plus-gen2x2", - } -} diff --git a/system/usbd/src/hc.rs b/system/usbd/src/hc.rs deleted file mode 100644 index 92dd30c9fa8..00000000000 --- a/system/usbd/src/hc.rs +++ /dev/null @@ -1,560 +0,0 @@ -//! The controller: its register window, the one DMA grant every ring and -//! context lives in, and the two rings every operation goes through. -//! -//! **Nothing here decides anything about a port or a device**: that is -//! `toyos-xhci`'s, driven from `bus`. What is here is xHCI 1.2's register file -//! and memory layout, written once: the §4.22.1 handoff, the §4.2 bring-up, -//! the command ring (§4.6), the event ring (§4.9.4) and the doorbells. -//! -//! **Every address the controller is told is one the claim's grant answered** -//! (`PciDev::dma_alloc`), so a slip here is a `DMA FAULT` record against this -//! process and never another's memory. - -use toyos::volatile::Window; -use toyos::{DmaRegion, PciDev}; -use toyos::shm::SharedMemory; -use toyos_abi::syscall::SyscallError; -use toyos_xhci::protocol::SupportedProtocol; -use toyos_xhci::xecp::{self, Handoff}; -use toyos_xhci::{Portsc, Protocols}; - -const CAP_CAPLENGTH: usize = 0x00; -const CAP_HCSPARAMS1: usize = 0x04; -const CAP_HCSPARAMS2: usize = 0x08; -const CAP_HCCPARAMS1: usize = 0x10; -const CAP_DBOFF: usize = 0x14; -const CAP_RTSOFF: usize = 0x18; -/// HCCPARAMS1 bit 0, 64-bit Addressing Capability (§5.3.6). -const HCCPARAMS1_AC64: u32 = 1 << 0; - -const OP_USBCMD: usize = 0x00; -const OP_USBSTS: usize = 0x04; -const OP_PAGESIZE: usize = 0x08; -const OP_CRCR: usize = 0x18; -const OP_DCBAAP: usize = 0x30; -const OP_CONFIG: usize = 0x38; -const OP_PORT_BASE: usize = 0x400; -const PORT_REG_SIZE: usize = 0x10; -/// PORTSC's Port Power (§5.4.8). -const PORTSC_PP: u32 = 1 << 9; - -/// USBCMD's Run/Stop, Host Controller Reset and Interrupter Enable; USBSTS's -/// HCHalted and Controller Not Ready (§5.4.1, §5.4.2). -const USBCMD_RS: u32 = 1 << 0; -const USBCMD_HCRST: u32 = 1 << 1; -const USBCMD_INTE: u32 = 1 << 2; -const USBSTS_HCH: u32 = 1 << 0; -const USBSTS_CNR: u32 = 1 << 11; - -/// Interrupter 0's registers in the runtime space (§5.5.2). -const IR0_IMAN: usize = 0x20; -const IR0_IMOD: usize = 0x24; -const IR0_ERSTSZ: usize = 0x28; -const IR0_ERSTBA: usize = 0x30; -const IR0_ERDP: usize = 0x38; -/// IMAN's Interrupt Pending (write 1 to clear) and Interrupt Enable. -const IMAN_IP_IE: u32 = 0b11; -/// ERDP's Event Handler Busy, written 1 to clear (§5.5.2.3.3). -const ERDP_EHB: u64 = 1 << 3; - -const TRB_CYCLE: u32 = 1; -const fn trb_type(t: u32) -> u32 { - t << 10 -} -pub const TRB_SETUP_STAGE: u32 = trb_type(2); -pub const TRB_DATA_STAGE: u32 = trb_type(3); -pub const TRB_STATUS_STAGE: u32 = trb_type(4); -const TRB_LINK: u32 = trb_type(6); -pub const TRB_ENABLE_SLOT: u32 = trb_type(9); -pub const TRB_DISABLE_SLOT: u32 = trb_type(10); -pub const TRB_ADDRESS_DEVICE: u32 = trb_type(11); -pub const TRB_EVALUATE_CONTEXT: u32 = trb_type(13); -pub const TRB_NO_OP_COMMAND: u32 = trb_type(23); -/// Link's Toggle Cycle; a Data or Status stage's Interrupt On Completion and -/// a Data stage's Interrupt on Short Packet; a Setup stage's Immediate Data. -const LINK_TC: u32 = 1 << 1; -pub const TRB_IOC: u32 = 1 << 5; -pub const TRB_ISP: u32 = 1 << 2; -pub const TRB_IDT: u32 = 1 << 6; - -const EVENT_TRANSFER: u32 = 32; -const EVENT_COMMAND_COMPLETION: u32 = 33; -const EVENT_PORT_STATUS_CHANGE: u32 = 34; - -/// The granularity everything in the grant is placed at; the controller's own -/// PAGESIZE must include it. -pub const PAGE: usize = 0x1000; -/// TRBs per ring: one page. -const RING_TRBS: usize = PAGE / 16; - -/// The grant's fixed head, one of each, since enumeration is serial. -const OFF_DCBAA: usize = 0; -const OFF_CMD_RING: usize = PAGE; -const OFF_ERST: usize = 2 * PAGE; -const OFF_EVENT_RING: usize = 3 * PAGE; -pub const OFF_INPUT_CTX: usize = 4 * PAGE; -pub const OFF_DATA: usize = 5 * PAGE; -const OFF_SCRATCH_ARRAY: usize = 6 * PAGE; -const OFF_SCRATCH: usize = 7 * PAGE; -/// Per device: its EP0 ring, then its output context. -const DEV_EP0_RING: usize = 0; -const DEV_OUT_CTX: usize = PAGE; -const DEV_STRIDE: usize = 2 * PAGE; - -/// The devices this controller is given room for, whatever more slots it has. -const MAX_DEVICES: usize = 32; -/// The scratchpad buffers one page of array can name. -const MAX_SCRATCH: usize = PAGE / 8; -/// A grant is placed in whole 2 MiB pages; asking for that is asking for -/// what the kernel will map anyway. -const GRANT_UNIT: usize = 2 * 1024 * 1024; - -/// How long a register the controller sets in microseconds may take. -/// Policy: the kernel driver's bound for the same waits. -const REGISTER_DEADLINE: std::time::Duration = std::time::Duration::from_millis(2_000); -/// How long firmware is given to let go of the controller (§4.22.1). Policy. -const HANDOFF_DEADLINE: std::time::Duration = std::time::Duration::from_secs(1); - -/// One TRB, as the rings carry it (§4.11). -#[derive(Clone, Copy, Debug, Default)] -pub struct Trb { - pub param: u64, - pub status: u32, - pub control: u32, -} - -/// What an event said, as far as the outstanding operation and the ports are -/// concerned. -#[derive(Clone, Copy, Debug)] -pub enum Event { - /// A Command Completion: the command TRB it names, its code and slot. - Command { trb: u64, code: u32, slot: u8 }, - /// A Transfer event: the TRB, the endpoint, its code and residue. - Transfer { trb: u64, slot: u8, dci: u8, code: u32, residue: u32 }, - /// A Port Status Change: a reason to read the ports, nothing more. - Port, - /// Anything else the controller may put on the ring. - Other, -} - -/// Why the controller was not brought up. -#[derive(Debug)] -pub enum Refusal { - Kernel(&'static str, SyscallError), - /// A register the controller published puts what it names outside the - /// window the claim maps. - Window(&'static str, usize), - /// A register bit that did not move within [`REGISTER_DEADLINE`]. - Stuck(&'static str), - PageSize(u32), - Scratchpad(usize), - /// HCCPARAMS1.AC64 clear: the controller would truncate the device - /// addresses of a grant, which the kernel does not place below 4 GiB. - Addresses32, -} - -impl std::fmt::Display for Refusal { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Kernel(what, why) => write!(f, "the kernel refused {what}: {why:?}"), - Self::Window(what, at) => write!(f, "{what} at {at:#x} is outside the register window"), - Self::Stuck(what) => write!(f, "{what} within {} ms", REGISTER_DEADLINE.as_millis()), - Self::PageSize(p) => write!(f, "PAGESIZE={p:#x} does not include 4 KiB, where every ring is placed"), - Self::Scratchpad(n) => write!(f, "it asks for {n} scratchpad buffers, past the {MAX_SCRATCH} one page names"), - Self::Addresses32 => write!(f, "it addresses only 32 bits (HCCPARAMS1.AC64 clear), and a grant is not placed below 4 GiB"), - } - } -} - -/// A ring this driver produces into: the command ring or an EP0 ring. -pub struct Ring { - window: Window, - base: u64, - tail: usize, - cycle: bool, -} - -impl Ring { - fn new(window: Window, base: u64) -> Self { - window.zero(); - let ring = Self { window, base, tail: 0, cycle: true }; - ring.put(RING_TRBS - 1, Trb { param: base, status: 0, control: TRB_LINK | LINK_TC }); - ring - } - - /// The TRB body first and the control word last, behind a release fence: - /// the control word carries the cycle bit, and the controller owns the TRB - /// the moment that bit matches its own (§4.9.2). - fn put(&self, at: usize, trb: Trb) { - let off = at * 16; - self.window.write(off, trb.param); - self.window.write(off + 8, trb.status); - std::sync::atomic::fence(std::sync::atomic::Ordering::Release); - self.window.write(off + 12, trb.control); - } - - /// Where the controller resumes, with the cycle state it must expect in bit 0. - pub fn dequeue(&self) -> u64 { - (self.base + (self.tail as u64) * 16) | u64::from(self.cycle) - } - - /// Put `trb` on the ring, and answer where it landed: the only name a - /// completion carries (§6.4.2.1–2). - pub fn enqueue(&mut self, mut trb: Trb) -> u64 { - trb.control = (trb.control & !TRB_CYCLE) | u32::from(self.cycle); - let at = self.base + (self.tail as u64) * 16; - self.put(self.tail, trb); - self.tail += 1; - if self.tail == RING_TRBS - 1 { - let link = TRB_LINK | LINK_TC | u32::from(self.cycle); - self.put(self.tail, Trb { param: self.base, status: 0, control: link }); - self.tail = 0; - self.cycle = !self.cycle; - } - at - } -} - -/// The controller, reset and running. -pub struct Controller { - pub dev: PciDev, - pub at: (u8, u8, u8), - /// Held for their mappings' lives. - _bar: SharedMemory, - _grant: DmaRegion, - regs: Window, - op: usize, - rt: usize, - db: usize, - grant: Window, - grant_base: u64, - pub max_ports: u8, - pub context_size: usize, - pub protocols: Protocols, - scratch: usize, - dev_blocks: usize, - cmd: Ring, - event_head: usize, - event_cycle: bool, -} - -impl Controller { - /// Claim to running: the §4.22.1 handoff, a reset, the grant, both rings - /// and interrupter 0, as §4.2 orders them. - pub fn open(dev: PciDev) -> Result { - let info = dev.describe().map_err(|e| Refusal::Kernel("the claim's description", e))?; - let at = (info.bus, info.dev, info.func); - // xHCI 1.2 §5.2.1: the capability registers are in BAR 0. - let bytes = info.bar_bytes[0]; - let bar = dev.map_bar(0, bytes).map_err(|e| Refusal::Kernel("BAR 0", e))?; - // SAFETY: the mapping is `bytes` long and lives as long as `bar`, which - // the controller holds for its own life. - let regs = unsafe { Window::new(bar.as_ptr(), bytes as usize) }; - let read = |at: usize| -> Option { (at + 4 <= regs.bytes()).then(|| regs.read::(at)) }; - - let cap_length = usize::from(regs.read::(CAP_CAPLENGTH)); - let hcsparams1 = regs.read::(CAP_HCSPARAMS1); - let hcsparams2 = regs.read::(CAP_HCSPARAMS2); - let hccparams1 = regs.read::(CAP_HCCPARAMS1); - let db = (regs.read::(CAP_DBOFF) & !0x3) as usize; - let rt = (regs.read::(CAP_RTSOFF) & !0x1f) as usize; - let max_slots = (hcsparams1 & 0xff) as usize; - let max_ports = (hcsparams1 >> 24) as u8; - let context_size = if hccparams1 & (1 << 2) != 0 { 64 } else { 32 }; - let scratch = (((hcsparams2 >> 21) & 0x1f) << 5 | ((hcsparams2 >> 27) & 0x1f)) as usize; - let op = cap_length; - for (what, end) in [ - ("the last port register", op + OP_PORT_BASE + usize::from(max_ports) * PORT_REG_SIZE), - ("interrupter 0", rt + IR0_ERDP + 8), - ("the last doorbell", db + 4 * (max_slots + 1)), - ] { - if end > regs.bytes() { - return Err(Refusal::Window(what, end)); - } - } - let pagesize = regs.read::(op + OP_PAGESIZE) & 0xffff; - if pagesize & 1 == 0 { - return Err(Refusal::PageSize(pagesize)); - } - if scratch > MAX_SCRATCH { - return Err(Refusal::Scratchpad(scratch)); - } - if hccparams1 & HCCPARAMS1_AC64 == 0 { - return Err(Refusal::Addresses32); - } - - take_from_firmware(®s, &read, hccparams1 >> 16); - let protocols = read_protocols(&read, hccparams1 >> 16, max_ports); - - let usbcmd = regs.read::(op + OP_USBCMD); - if usbcmd & USBCMD_RS != 0 { - regs.write::(op + OP_USBCMD, usbcmd & !USBCMD_RS); - } - settle("it never halted", || regs.read::(op + OP_USBSTS) & USBSTS_HCH != 0)?; - regs.write::(op + OP_USBCMD, USBCMD_HCRST); - settle("it held HCRST", || regs.read::(op + OP_USBCMD) & USBCMD_HCRST == 0)?; - settle("it stayed Controller Not Ready", || regs.read::(op + OP_USBSTS) & USBSTS_CNR == 0)?; - - // After the reset, so a controller refused above never had a grant. - // AC64 is required above, so its addresses are 64 bits (§5.3.6). - dev.dma_limit(64).map_err(|e| Refusal::Kernel("the address limit", e))?; - let dev_blocks = max_slots.min(MAX_DEVICES); - let bytes = (OFF_SCRATCH + scratch * PAGE + dev_blocks * DEV_STRIDE).next_multiple_of(GRANT_UNIT); - let region = dev.dma_alloc(bytes as u64).map_err(|e| Refusal::Kernel("a DMA grant", e))?; - // SAFETY: the kernel rounds a grant up and never down, so it covers - // `bytes`, and lives as long as `region`, which the controller holds. - let grant = unsafe { Window::new(region.memory.as_ptr(), bytes) }; - grant.zero(); - let base = region.device_addr; - // Halted and reset with mastering off: started before it is told any - // address, which a controller may read the moment it is written - // (QEMU's reads the segment table on the ERSTBA write). - dev.dma_start().map_err(|e| Refusal::Kernel("the bus-master start", e))?; - println!( - "usbd: PCI {:02x}:{:02x}.{} reset: {max_ports} ports, {max_slots} slots ({dev_blocks} given room), \ - {context_size}-byte contexts, {scratch} scratchpad buffers, a {} KiB grant", - at.0, - at.1, - at.2, - bytes / 1024 - ); - - regs.write::(op + OP_CONFIG, dev_blocks as u32); - for i in 0..scratch { - grant.write::(OFF_SCRATCH_ARRAY + i * 8, base + (OFF_SCRATCH + i * PAGE) as u64); - } - // DCBAA[0] names the scratchpad array (§6.1), and only once it is filled. - grant.write::(OFF_DCBAA, if scratch > 0 { base + OFF_SCRATCH_ARRAY as u64 } else { 0 }); - regs.write::(op + OP_DCBAAP, base + OFF_DCBAA as u64); - let cmd = Ring::new(grant.sub(OFF_CMD_RING, PAGE), base + OFF_CMD_RING as u64); - regs.write::(op + OP_CRCR, cmd.dequeue()); - // One segment, the whole event ring (§6.5), named before ERSTBA is. - grant.write::(OFF_ERST, base + OFF_EVENT_RING as u64); - grant.write::(OFF_ERST + 8, RING_TRBS as u32); - regs.write::(rt + IR0_ERSTSZ, 1); - regs.write::(rt + IR0_ERDP, base + OFF_EVENT_RING as u64); - regs.write::(rt + IR0_ERSTBA, base + OFF_ERST as u64); - // No moderation: an event is an interrupt, which is what this driver - // counts against the records it reads. - regs.write::(rt + IR0_IMOD, 0); - regs.write::(rt + IR0_IMAN, IMAN_IP_IE); - regs.write::(op + OP_USBCMD, USBCMD_RS | USBCMD_INTE); - settle("it stayed halted after Run", || regs.read::(op + OP_USBSTS) & USBSTS_HCH == 0)?; - - let ctrl = Self { - dev, - at, - _bar: bar, - _grant: region, - regs, - op, - rt, - db, - grant, - grant_base: base, - max_ports, - context_size, - protocols, - scratch, - dev_blocks, - cmd, - event_head: 0, - event_cycle: true, - }; - // Port Power Control leaves a port unpowered after HCRST, and an - // unpowered port reports nothing; without PPC the write does nothing. - for p in 0..max_ports { - let portsc = ctrl.portsc(p); - if portsc.raw() & PORTSC_PP == 0 { - ctrl.write_portsc(p, portsc.neutral().powered()); - } - } - Ok(ctrl) - } - - pub fn portsc(&self, port: u8) -> Portsc { - Portsc::from_raw(self.regs.read::(self.op + OP_PORT_BASE + usize::from(port) * PORT_REG_SIZE)) - } - - pub fn write_portsc(&self, port: u8, write: toyos_xhci::portsc::Write) { - self.regs.write::(self.op + OP_PORT_BASE + usize::from(port) * PORT_REG_SIZE, write.raw()); - } - - /// Put a command on the ring and ring the host controller's doorbell; - /// answers the TRB its completion will name. - pub fn command(&mut self, trb: Trb) -> u64 { - let at = self.cmd.enqueue(trb); - self.regs.write::(self.db, 0); - at - } - - /// Ring `slot`'s doorbell for endpoint `dci`. - pub fn doorbell(&self, slot: u8, dci: u8) { - self.regs.write::(self.db + 4 * usize::from(slot), u32::from(dci)); - } - - /// The next event, or `None` once the ring holds nothing new. A drain - /// ends with [`Self::consumed`]. - pub fn next_event(&mut self) -> Option { - let at = OFF_EVENT_RING + self.event_head * 16; - let control = self.grant.read::(at + 12); - if (control & TRB_CYCLE != 0) != self.event_cycle { - return None; - } - std::sync::atomic::fence(std::sync::atomic::Ordering::Acquire); - let param = self.grant.read::(at); - let status = self.grant.read::(at + 8); - self.event_head += 1; - if self.event_head == RING_TRBS { - self.event_head = 0; - self.event_cycle = !self.event_cycle; - } - let code = status >> 24; - let slot = (control >> 24) as u8; - Some(match (control >> 10) & 0x3f { - EVENT_COMMAND_COMPLETION => Event::Command { trb: param & !0xf, code, slot }, - EVENT_TRANSFER => Event::Transfer { - trb: param & !0xf, - slot, - dci: ((control >> 16) & 0x1f) as u8, - code, - residue: status & 0x00ff_ffff, - }, - EVENT_PORT_STATUS_CHANGE => Event::Port, - _ => Event::Other, - }) - } - - /// Whether an event is waiting that nothing has taken off the ring. - pub fn event_waiting(&self) -> bool { - let control = self.grant.read::(OFF_EVENT_RING + self.event_head * 16 + 12); - (control & TRB_CYCLE != 0) == self.event_cycle - } - - /// Tell the controller every event up to here is taken, and clear Event - /// Handler Busy and Interrupt Pending, so the next event interrupts again - /// (§4.17.2): one write per drain, not per event. - pub fn consumed(&self) { - let erdp = self.grant_base + (OFF_EVENT_RING + self.event_head * 16) as u64; - self.regs.write::(self.rt + IR0_ERDP, erdp | ERDP_EHB); - self.regs.write::(self.rt + IR0_IMAN, IMAN_IP_IE); - } - - /// The grant from `off`, `len` bytes long, and where the device reaches it. - pub fn grant(&self, off: usize, len: usize) -> (Window, u64) { - (self.grant.sub(off, len), self.grant_base + off as u64) - } - - /// A slot's EP0 ring and output context, or `None` for a slot past the - /// room the grant gives devices. `slot` is the controller's answer. - pub fn device_block(&self, slot: u8) -> Option { - let index = usize::from(slot).checked_sub(1).filter(|i| *i < self.dev_blocks)?; - Some(OFF_SCRATCH + self.scratch * PAGE + index * DEV_STRIDE) - } - - /// A fresh EP0 ring in `slot`'s block, and its output context named in - /// the DCBAA. - pub fn bind_slot(&self, block: usize, slot: u8) -> Ring { - let (out, out_addr) = self.grant(block + DEV_OUT_CTX, PAGE); - out.zero(); - self.grant.write::(OFF_DCBAA + usize::from(slot) * 8, out_addr); - let (ring, ring_addr) = self.grant(block + DEV_EP0_RING, PAGE); - Ring::new(ring, ring_addr) - } - - /// A slot the controller has disabled names no context any more. - pub fn unbind_slot(&self, slot: u8) { - self.grant.write::(OFF_DCBAA + usize::from(slot) * 8, 0); - } - - /// One dword of context `index` of the input context, for a command about - /// to name it. - pub fn input_ctx(&self, index: usize, dword: usize, value: u32) { - self.grant.write::(OFF_INPUT_CTX + index * self.context_size + dword * 4, value); - } - - pub fn clear_input_ctx(&self) { - self.grant.sub(OFF_INPUT_CTX, PAGE).zero(); - } -} - -/// §4.22.1: ask firmware for the controller, give it [`HANDOFF_DEADLINE`] to -/// let go, and turn every SMI off whatever it answered — a controller is reset -/// from under firmware rather than left to it, with the line naming which. -fn take_from_firmware(regs: &Window, read: &dyn Fn(usize) -> Option, xecp_dwords: u32) { - let walk_read = |at: u64| read(at as usize); - let legsup = match xecp::find(&walk_read, xecp_dwords, xecp::CAP_ID_LEGACY) { - Ok(Some(at)) if (at + xecp::LEGCTLSTS + 4) as usize <= regs.bytes() => at as usize, - Ok(Some(at)) => return println!("usbd: USB Legacy Support at {at:#x} runs past the register window — no handoff"), - Ok(None) => return println!("usbd: no USB Legacy Support capability, nothing to hand over"), - Err(why) => return println!("usbd: the extended capability list does not walk ({why:?}) — no handoff"), - }; - let before = regs.read::(legsup); - regs.write::(legsup, before | xecp::LEGSUP_OS_OWNED); - let asked = std::time::Instant::now(); - let mut now = regs.read::(legsup); - // Firmware answers in its own SMI handler and raises nothing a process - // can wait on, so its semaphore is read until it clears or the bound passes. - while now & xecp::LEGSUP_BIOS_OWNED != 0 && asked.elapsed() < HANDOFF_DEADLINE { - std::hint::spin_loop(); - now = regs.read::(legsup); - } - match xecp::handoff(before, now) { - Handoff::NeverClaimed => println!("usbd: firmware did not claim the controller (USBLEGSUP {before:#010x})"), - Handoff::Released => println!( - "usbd: firmware released the controller in {} us (USBLEGSUP {before:#010x} -> {now:#010x})", - asked.elapsed().as_micros() - ), - Handoff::Kept => println!( - "usbd: firmware still owns the controller after {} ms (USBLEGSUP {before:#010x} -> {now:#010x}) — \ - resetting it anyway", - HANDOFF_DEADLINE.as_millis() - ), - } - let ctl = regs.read::(legsup + xecp::LEGCTLSTS as usize); - regs.write::(legsup + xecp::LEGCTLSTS as usize, xecp::smis_off(ctl)); - let after = regs.read::(legsup + xecp::LEGCTLSTS as usize); - println!("usbd: USBLEGCTLSTS {ctl:#010x} -> {after:#010x} (SMI generation off)"); -} - -/// What each port register speaks, from every Supported Protocol capability. -fn read_protocols(read: &dyn Fn(usize) -> Option, xecp_dwords: u32, max_ports: u8) -> Protocols { - let mut protocols = Protocols::UNKNOWN; - let walk_read = |at: u64| read(at as usize); - let walked = xecp::for_each(&walk_read, xecp_dwords, xecp::CAP_ID_PROTOCOL, &mut |at| { - let at = at as usize; - let (Some(dw0), Some(dw1), Some(dw2)) = (read(at), read(at + 4), read(at + 8)) else { - return println!("usbd: a Supported Protocol capability at {at:#x} runs past the register window"); - }; - match SupportedProtocol::decode(dw0, dw1, dw2, max_ports) { - Ok(found) => { - println!( - "usbd: USB {}.{:x} on ports {}..={}", - found.major, - found.minor >> 4, - found.first_port + 1, - found.first_port + found.port_count - ); - protocols.record(&found); - } - Err(why) => println!("usbd: a Supported Protocol capability at {at:#x} is unusable: {why:?}"), - } - }); - if let Err(why) = walked { - println!("usbd: the extended capability list does not walk: {why:?}"); - } - protocols -} - -/// Read until `done` or [`REGISTER_DEADLINE`]: the controller sets these bits -/// in microseconds and raises no interrupt for any of them (§5.4.1, §5.4.2). -fn settle(what: &'static str, done: impl Fn() -> bool) -> Result<(), Refusal> { - let began = std::time::Instant::now(); - while !done() { - if began.elapsed() >= REGISTER_DEADLINE { - return Err(Refusal::Stuck(what)); - } - std::hint::spin_loop(); - } - Ok(()) -} diff --git a/system/usbd/src/main.rs b/system/usbd/src/main.rs index 8bb263c8476..d4e3f7ff104 100644 --- a/system/usbd/src/main.rs +++ b/system/usbd/src/main.rs @@ -1,38 +1,45 @@ -//! usbd: one xHCI controller, driven from userland. +//! usbd: one xHCI controller and every device on it, driven from userland. //! -//! **It holds a claim on one PCI function and nothing else of the machine.** -//! The claim arrives under the `dev:pci:` label its starter minted it under, -//! and the port it answers `inspect` on under `serve:usb`; the kernel keeps -//! config space, the interrupt vector and the function's address space, and -//! usbd never names an address its grant did not answer (`hc`). +//! **It holds a claim on one PCI function** (`dev:pci:`), the acceptor it +//! answers `inspect` on, and what its row receives: the block service's +//! `disk` port, where its sticks are offered and their sessions handed over, +//! and the owner of input's `input` port, where its keyboards and pointers +//! report. The kernel keeps config space, the interrupt vector and the +//! function's address space; every decision is `toyos-xhci`'s, and this +//! binary is its boundary one instruction deep (`toyos-pci-claim`). //! -//! **One thread waits on one poller and nowhere else**: the claim's interrupt -//! records, the port's connections, and the next deadline the ports or the -//! outstanding operation hand back (`bus`). With none of those, it is parked -//! until one of the first two wakes it. Bring-up alone spins, on register bits -//! the controller raises no interrupt for, each bounded. +//! **One thread waits on one poller and nowhere else**: the claim's +//! interrupt records, the block service's link and each session's doorbell, +//! the owner of input's connection while it is full, `inspect`'s clients, and +//! the next deadline the driver's tasks hand back. //! -//! **What it does with a controller** is the §4.22.1 handoff, a reset, its -//! rings in one grant, a No-Op that says the command ring and the interrupt -//! both work, and every port's device enumerated and named; no class is bound. +//! **The event ring is read only when an interrupt record says so**, so an +//! answer the controller wrote and no interrupt announced is found only by +//! its operation's deadline, and counted (`events.unannounced`). //! -//! **A server never blocks on a client.** Accept and the request are two -//! events, the request is a bare header buffered until whole, and the answer -//! is one `try_send` whose refusal drops the client by name. +//! **A server never blocks on a client**: every answer is one `try_send`, +//! and the owner of input is never waited on (`toyos_xhci::input`). +use std::collections::BTreeMap; use std::time::{Duration, Instant}; -use toyos::endow::{self, Endowments}; -use toyos::ipc::{Connection, FrameRx, RxStep}; -use toyos::poller::{Poller, READABLE}; -use toyos::AsHandle; +use toyos::endow::{self, EndowError, Endowments}; +use toyos::ipc::{Connection, FrameRx, RxStep, TrySendError}; +use toyos::poller::{Poller, READABLE, WRITABLE}; +use toyos::{AsHandle, PciDev}; use toyos_abi::syscall::{SyscallError, DEV_PREFIX}; +use toyos_blockring::disk::Settled; +use toyos_blockserve::link::Link; +use toyos_blockserve::sessions::{Owner as Request, MAX_SESSIONS}; +use toyos_blockserve::{table, Device, Partition, Sessions}; +use toyos_input::wire::{self, Record, MAX_PAYLOAD}; use toyos_inspect::{Snapshot, MSG_INSPECT, MSG_SNAPSHOT}; - -mod bus; -mod hc; - -use bus::Bus; +use toyos_pci_claim::{Bar, Grant}; +use toyos_xhci::exec::{Dead, Host, Nanos, Tasks}; +use toyos_xhci::hc::Reset; +use toyos_xhci::input::{Opened, Owner, Sent}; +use toyos_xhci::msc::{Event, Work}; +use toyos_xhci::usb::{self, Config, Usb}; /// Connections accepted and not yet asked, and how long one may stay so. const MAX_PENDING: usize = 8; @@ -40,20 +47,91 @@ const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(2); const TOKEN_IRQ: u64 = 0; const TOKEN_ACCEPT: u64 = 1; +const TOKEN_LINK: u64 = 2; +const TOKEN_INPUT: u64 = 3; +const TOKEN_STOP: u64 = 4; const TOKEN_PENDING: u64 = 0x1_0000; +/// A session's doorbell: its disk in bits 16 up, its id below. +const TOKEN_SESSION: u64 = 0x1_0000_0000; + +/// The process the driver runs in: its clock, and its lines. +#[derive(Clone)] +struct Process { + began: Instant, + name: String, +} + +impl Host for Process { + fn now(&self) -> Nanos { + self.began.elapsed().as_nanos() as Nanos + } + fn say(&self, line: core::fmt::Arguments<'_>) { + println!("{}: {line}", self.name); + } +} -/// What woke the loop, which `inspect` reads: a loop with no deadline wakes -/// only for the claim or a client. +type Driver = Usb, String>>; + +/// The owner of input, over the `input` port. #[derive(Default)] -struct Wakes { - /// The wait ended on its timeout. - timed: u64, - /// The claim read ready. - device: u64, - /// The claim read ready with no record behind it. - empty: u64, - /// A client connected or spoke. - client: u64, +struct InputPort { + conn: Option, +} + +impl Owner for InputPort { + fn open(&mut self) -> Opened { + match endow::service(wire::PORT) { + Ok(conn) => { + self.conn = Some(conn); + Opened::Opened + } + Err(EndowError::NotEndowed) => Opened::NotGiven, + Err(_) => Opened::Refused, + } + } + fn send(&mut self, record: &Record) -> Sent { + let Some(conn) = &self.conn else { return Sent::Ended }; + let mut payload = [0u8; MAX_PAYLOAD]; + let (msg_type, len) = record.frame(&mut payload); + match conn.try_send_bytes(msg_type, &payload[..len]) { + Ok(()) => Sent::Taken, + // Nothing went: a frame is sent whole or not at all. + Err(TrySendError::Full) => Sent::Full, + Err(_) => Sent::Ended, + } + } + fn close(&mut self) { + self.conn = None; + } +} + +/// One disk, as its sessions reach the driver. +struct Disk<'a> { + usb: &'a Driver, + disk: u32, + claim: &'a PciDev, +} + +impl Device for Disk<'_> { + fn has_room(&self) -> bool { + self.usb.disks.has_room(self.disk) + } + fn submit_io(&mut self, write: bool, block: u64, blocks: u32, at: u64, owner: Request) { + self.usb.disks.submit(self.disk, Work::Io { write, block, blocks, at, token: (owner.session, owner.tag) }); + } + fn submit_flush(&mut self, owner: Request) -> bool { + let cached = self.usb.disks.cached(self.disk); + if cached { + self.usb.disks.submit(self.disk, Work::Flush { token: (owner.session, owner.tag) }); + } + cached + } + fn dma_map(&mut self, region: toyos::RawHandle) -> Result { + self.claim.dma_map(region).map(|m| m.device_addr) + } + fn dma_unmap(&mut self, device_addr: u64) -> Result<(), SyscallError> { + self.claim.dma_unmap(device_addr) + } } struct Pending { @@ -63,7 +141,7 @@ struct Pending { } /// The one `dev:pci:` claim this process was started with. -fn claim() -> toyos::PciDev { +fn claim() -> PciDev { let label = Endowments::get() .labels() .find(|l| l.strip_prefix(DEV_PREFIX).is_some_and(|name| name.starts_with("pci:"))) @@ -73,126 +151,343 @@ fn claim() -> toyos::PciDev { } fn main() { - let began = Instant::now(); - let now = || began.elapsed().as_nanos() as u64; - let acceptor = endow::acceptor(toyos_inspect::USB.port) - .unwrap_or_else(|| panic!("usbd: started serving no `{}` port", toyos_inspect::USB.port)); - let ctrl = hc::Controller::open(claim()) - .unwrap_or_else(|why| panic!("usbd: the controller this process was given cannot be driven — {why}")); - let mut bus = Bus::new(ctrl, now()); - - let poller = Poller::new(2 + MAX_PENDING as u32); + let dev = claim(); + let info = match toyos_pci_claim::describe(&dev) { + Ok(info) => info, + Err(why) => not_serving("usbd", &format!("the claim would not describe itself: {why}"), Link::new("usbd".into()), None), + }; + let at = format!("{:02x}:{:02x}.{}", info.bus, info.dev, info.func); + let host = Process { began: Instant::now(), name: format!("usbd {at}") }; + let name = host.name.clone(); + let acceptor = endow::acceptor(toyos_inspect::USB.port); + let stop = Endowments::get().take::(toyos_quiesce::DRIVER_STOP); + let link = Link::new(name.clone()); + // xHCI 1.2 §5.2.1: the capability registers are in BAR 0. + let (bar, _bar) = match Bar::map(&dev, 0, info.bar_bytes[0]) { + Ok(mapped) => mapped, + Err(why) => not_serving(&name, &format!("its registers would not map: {why}"), link, stop), + }; + let reset = match Reset::new(bar, &host) { + Ok(reset) => reset, + Err(why) => not_serving(&name, &format!("the controller cannot be driven: {why}"), link, stop), + }; + // After the reset, so a controller refused above never had a grant, and + // the addresses every grant and lend is placed below stated first. + if let Err(why) = dev.dma_limit(reset.address_bits) { + not_serving(&name, &format!("the kernel refused its {}-bit address limit: {why:?}", reset.address_bits), link, stop) + } + let bytes = reset.grant_bytes(); + let (grant, _region) = match Grant::alloc(&dev, bytes as u64) { + Ok(granted) => granted, + Err(why) => not_serving(&name, &format!("the kernel refused its grant: {why}"), link, stop), + }; + // Halted and reset with its mastering off, and started before it is told + // any address, which a controller may read the moment it is written. + if let Err(why) = dev.dma_start() { + not_serving(&name, &format!("the kernel would not start its bus mastering: {why:?}"), link, stop) + } + let hc = match reset.start(grant, &host) { + Ok(hc) => hc, + Err(why) => not_serving(&name, &format!("the controller would not run: {why}"), link, stop), + }; + println!("{name}: running, {} ports, a {} KiB grant", hc.max_ports, bytes / 1024); + let config = Config { input: endow::namespace().is_some(), disks: link.is_some() }; + let usb: Driver = Usb::new(hc, host, config, table::read); + serve(&name, &dev, &usb, acceptor, link, stop) +} + +/// A refusal this driver can name, before it serves anything: said once, the +/// block service told this controller is unusable, and nothing served. +fn not_serving(name: &str, why: &str, mut link: Option, stop: Option) -> ! { + println!("{name}: NOT SERVING — {why}"); + if let Some(link) = &mut link { + link.settle(Settled::Unusable); + } + idle(link, stop) +} + +/// A controller that cannot be used: it keeps its link, so the service goes +/// on saying so, and serves nothing; it ends when the machine stops. +fn idle(mut link: Option, stop: Option) -> ! { + let poller = Poller::new(2); + loop { + if let Some(link) = &mut link { + link.watch(&poller, TOKEN_LINK); + } + if let Some(stop) = &stop { + poller.watch(stop, READABLE, TOKEN_STOP); + } + let mut stopping = false; + poller.wait(1, u64::MAX, |token| stopping |= token == TOKEN_STOP); + if stopping { + std::process::exit(0); + } + for handed in link.iter_mut().flat_map(Link::take) { + toyos_abi::syscall::close(handed.region); + } + } +} + +/// What woke the loop, which `inspect` reads: a loop with nothing to wait +/// on wakes only for the claim or a client. +#[derive(Default)] +struct Wakes { + /// The wait ended on its timeout. + timed: u64, + /// The claim read ready, and with no record behind it. + device: u64, + empty: u64, + /// Interrupt records read off the claim. + records: u64, +} + +fn serve( + name: &str, + dev: &PciDev, + usb: &Driver, + acceptor: Option, + mut link: Option, + stop: Option, +) -> ! { + let mut tasks = usb.tasks(); + let mut disks: BTreeMap = BTreeMap::new(); + let mut input = InputPort::default(); + let mut settled = false; + let poller = Poller::new(5 + MAX_PENDING as u32 + 4 * MAX_SESSIONS as u32); let mut pending: Vec = Vec::new(); - let mut wakes = Wakes::default(); let mut ready: Vec = Vec::new(); - // Bring-up left the No-Op outstanding, so the first quiet is a change. - let mut quiet = false; + let mut wakes = Wakes::default(); + let mut wake_at; loop { - let wake_at = bus.pass(now()); - if wake_at.is_none() && !quiet { - println!("usbd: settled, {} device(s) named", bus.devices.len()); + // The driver's turn: every task run. + wake_at = match tasks.run(&usb.wake) { + Ok(at) => at, + Err(Dead(why)) => ended(name, &format!("{why}; nothing drives the controller from here")), + }; + for event in usb.disks.take_events() { + match event { + Event::Arrived { disk, name: disk_name, table } => { + let label = format!("{name} {disk_name}"); + let sessions = Sessions::new(label.clone(), table::say(&label, table)); + if let Some(link) = &mut link { + link.offer(disk, sessions.offered()); + } + disks.insert(disk, sessions); + } + Event::Answered { disk, token: (session, tag), write, ok, status } => { + disks.get_mut(&disk).expect("an answer for an offered disk").answered(Request { session, tag, write }, ok, status); + } + Event::Reset { disk } => { + let answered = disks.get_mut(&disk).expect("an offered disk").reset(); + println!("{name}: disk {disk} came back from a reset that may have lost its cache; {answered} request(s) answered not done"); + } + Event::Gone { disk } => { + if let Some(link) = &mut link { + link.gone(disk); + } + if let Some(mut sessions) = disks.remove(&disk) { + sessions.end(&mut Disk { usb, disk, claim: dev }); + } + } + } + } + if !settled && usb.settled() { + settled = true; + println!("{name}: settled, {} device(s) named, {} disk(s) offered", usb.named.borrow().len(), disks.len()); + if let Some(link) = &mut link { + link.settle(Settled::Offered); + } + } + for handed in link.iter_mut().flat_map(Link::take) { + match disks.get_mut(&handed.session.disk) { + Some(sessions) => { + let mut port = Disk { usb, disk: handed.session.disk, claim: dev }; + sessions.open(&mut port, handed.conn, handed.region, handed.session.unique, handed.session.writes); + } + None => { + println!("{name}: the block service handed a session on disk {}, which is not served", handed.session.disk); + toyos_abi::syscall::close(handed.region); + } + } + } + let submitted = usb.disks.submitted(); + for (&disk, sessions) in disks.iter_mut() { + for id in sessions.ids() { + if ready.contains(&(TOKEN_SESSION | u64::from(disk) << 16 | id)) { + sessions.hear(id); + } + } + let mut port = Disk { usb, disk, claim: dev }; + sessions.pull(&mut port); + sessions.publish(); + sessions.retire(&mut port); + } + usb.input.borrow_mut().flush(&mut input, usb.wake.now()); + + // inspect's clients. + if let (true, Some(acceptor)) = (ready.contains(&TOKEN_ACCEPT), &acceptor) { + match acceptor.accept() { + Ok(conn) if pending.len() < MAX_PENDING => pending.push(Pending { conn, rx: FrameRx::new(), since: Instant::now() }), + Ok(conn) => println!("{name}: refusing client {} — {MAX_PENDING} are already waiting", conn.as_handle().0), + Err(why) => println!("{name}: the kernel refused a client's connection: {why:?}"), + } + } + pending.retain_mut(|p| { + if p.since.elapsed() >= HANDSHAKE_TIMEOUT { + return false; + } + if !ready.contains(&(TOKEN_PENDING + u64::from(p.conn.as_handle().0))) { + return true; + } + match p.rx.pump(&p.conn) { + RxStep::Idle => true, + RxStep::Frame { msg_type: MSG_INSPECT, .. } => { + answer(&p.conn, usb, &wakes, wake_at); + false + } + _ => false, + } + }); + + // Work a session queued is the driver's to run before anything waits. + if usb.disks.submitted() != submitted { + ready.clear(); + continue; + } + poller.watch(dev, READABLE, TOKEN_IRQ); + if let Some(stop) = &stop { + poller.watch(stop, READABLE, TOKEN_STOP); + } + if let Some(acceptor) = &acceptor { + poller.watch(acceptor, READABLE, TOKEN_ACCEPT); + } + if let Some(link) = &link { + link.watch(&poller, TOKEN_LINK); + } + if let Some(conn) = &input.conn { + if usb.input.borrow().full() { + poller.watch(conn, WRITABLE, TOKEN_INPUT); + } + } + for (&disk, sessions) in &disks { + sessions.watch(&poller, TOKEN_SESSION | u64::from(disk) << 16); } - quiet = wake_at.is_none(); - poller.watch(&bus.ctrl.dev, READABLE, TOKEN_IRQ); - poller.watch(&acceptor, READABLE, TOKEN_ACCEPT); for p in &pending { poller.watch(&p.conn, READABLE, TOKEN_PENDING + u64::from(p.conn.as_handle().0)); } - let mut timeout = wake_at.map_or(u64::MAX, |at| at.saturating_sub(now())); + let now = usb.wake.now(); + let mut timeout = wake_at.map_or(u64::MAX, |at| at.saturating_sub(now)); if let Some(first) = pending.iter().map(|p| p.since).min() { - let left = HANDSHAKE_TIMEOUT.saturating_sub(first.elapsed()); - timeout = timeout.min(left.as_nanos() as u64); + timeout = timeout.min(HANDSHAKE_TIMEOUT.saturating_sub(first.elapsed()).as_nanos() as u64); } ready.clear(); poller.wait(1, timeout, |token| ready.push(token)); if ready.is_empty() { wakes.timed += 1; } - + if ready.contains(&TOKEN_STOP) { + end_for_the_stop(name, usb, tasks); + } // **The record is taken, not merely noticed**: a claim reads ready // while it holds one, so one left would wake every wait after it. if ready.contains(&TOKEN_IRQ) { wakes.device += 1; - match bus.ctrl.dev.irq() { - Ok(record) => bus.interrupt(u64::from(record.count)), + match dev.irq() { + Ok(_) => { + wakes.records += 1; + while usb.hc.take_events() && usb.hc.event_waiting() {} + } Err(SyscallError::WouldBlock) => wakes.empty += 1, - // The kernel's word that the function is no longer this - // process's: a fault at the unit is the one that happens. - Err(why) => panic!("usbd: the claim refused its interrupt record ({why:?}); nothing drives it from here"), + Err(why) => ended(name, &format!("the claim refused its interrupt record ({why:?}); nothing drives it from here")), } } + } +} - if ready.contains(&TOKEN_ACCEPT) { - wakes.client += 1; - match acceptor.accept() { - Ok(conn) if pending.len() < MAX_PENDING => pending.push(Pending { conn, rx: FrameRx::new(), since: Instant::now() }), - Ok(conn) => println!("usbd: refusing client {} — {MAX_PENDING} are already waiting", conn.as_handle().0), - Err(why) => println!("usbd: the kernel refused a client's connection: {why:?}"), - } - } - let mut i = 0; - while i < pending.len() { - let token = TOKEN_PENDING + u64::from(pending[i].conn.as_handle().0); - if pending[i].since.elapsed() >= HANDSHAKE_TIMEOUT { - println!("usbd: dropping client {} — it never asked", pending[i].conn.as_handle().0); - pending.remove(i); - continue; - } - if !ready.contains(&token) { - i += 1; - continue; - } - wakes.client += 1; - let p = &mut pending[i]; - match p.rx.pump(&p.conn) { - RxStep::Idle => i += 1, - RxStep::Eof => { - pending.remove(i); - } - RxStep::Frame { msg_type: MSG_INSPECT, .. } => { - let p = pending.remove(i); - answer(&p.conn, &bus, &wakes, wake_at, now()); - } - RxStep::Frame { .. } | RxStep::Malformed => { - println!("usbd: dropping client {} — it asked something that is not `inspect`", p.conn.as_handle().0); - pending.remove(i); - } - } +/// The supervisor says the machine stops, every file server synced: every +/// device handed back with a port reset and the controller left reset +/// (`Hc::stop`), and then this process ends, which releases the claim. What +/// a task still held is let go of: the driver ends here. +fn end_for_the_stop<'u>(name: &str, usb: &'u Driver, tasks: Tasks<'u>) -> ! { + usb.hc.ending.set(true); + drop(tasks); + let stopped = std::cell::Cell::new(None); + let mut stop = Tasks::default(); + stop.spawn(async { + stopped.set(Some(usb.hc.stop(&usb.wake).await)); + Ok(()) + }); + while !stop.is_empty() { + match stop.run(&usb.wake) { + Ok(Some(at)) => std::thread::sleep(Duration::from_nanos(at.saturating_sub(usb.wake.now()))), + Ok(None) => {} + Err(Dead(why)) => ended(name, &format!("{why}, in the stop")), } } + let stopped = stopped.get().expect("the stop's task set it before it ended"); + println!("{name}: the machine stops: {stopped}"); + // **The exit code is the account that is sure to be read**: the kernel + // records it, and this line may reach no log, whose volume this process + // just took down. + std::process::exit(if stopped.whole() { 0 } else { STOP_UNFINISHED }) +} + +/// The controller stopped answering, or the claim refused it, while usbd +/// served it: said by name, and this process ends, which takes every device +/// on the controller with it and releases the claim, so the supervisor may +/// drive the function again. +fn ended(name: &str, why: &str) -> ! { + println!("{name}: ENDED — {why}"); + std::process::exit(CONTROLLER_LOST) } -fn answer(conn: &Connection, bus: &Bus, wakes: &Wakes, wake_at: Option, now: u64) { +/// What usbd ends with when the controller it served is lost ([`ended`]). +const CONTROLLER_LOST: i32 = 2; + +/// What usbd ends with when its stop left a port in reset or the controller +/// running: [`toyos_xhci::hc::Stopped::whole`] false. +const STOP_UNFINISHED: i32 = 3; + +fn answer(conn: &Connection, usb: &Driver, wakes: &Wakes, wake_at: Option) { let mut snap = Snapshot::new(toyos_inspect::USB); - let (bus_no, dev, func) = bus.ctrl.at; - snap.put("controller", format!("{bus_no:02x}:{dev:02x}.{func}")); - snap.put("ports", u32::from(bus.ctrl.max_ports)); - snap.put("devices", bus.devices.len()); - for named in &bus.devices { - let at = format!("device.{}", named.port + 1); - let class = named.class(); - snap.put(&format!("{at}.id"), format!("{:04x}:{:04x}", named.device.vendor, named.device.product)); - snap.put(&format!("{at}.speed"), bus::speed_name(named.speed)); - snap.put(&format!("{at}.class"), format!("{:02x}:{:02x}:{:02x}", class.class, class.subclass, class.protocol)); - snap.put(&format!("{at}.slot"), u32::from(named.slot)); - } - let counts = &bus.counts; - snap.put("noop", match counts.noop { + snap.put("ports", u32::from(usb.hc.max_ports)); + let named = usb.named.borrow(); + snap.put("devices", named.len()); + for d in named.values() { + let at = format!("device.{}", d.port + 1); + snap.put(&format!("{at}.id"), format!("{:04x}:{:04x}", d.device.vendor, d.device.product)); + snap.put(&format!("{at}.speed"), usb::speed_name(d.speed)); + snap.put(&format!("{at}.slot"), u32::from(d.slot)); + snap.put(&format!("{at}.functions"), d.functions.join("; ")); + } + snap.put("noop", match usb.noop.get() { None => "outstanding", Some(true) => "completed", Some(false) => "unanswered", }); - snap.put("interrupts.records", counts.records); - snap.put("interrupts.counted", counts.interrupts); - snap.put("events.taken", counts.events); - snap.put("events.unannounced", counts.unannounced); - snap.put("events.other", counts.other); - snap.put("operations.silent", counts.silent); + let hc = usb.hc.counts.get(); + snap.put("interrupts.records", wakes.records); + snap.put("events.taken", hc.events); + snap.put("events.unannounced", hc.unannounced); + snap.put("events.stray", hc.stray); + snap.put("events.other", hc.other); + snap.put("operations.silent", hc.silent); + snap.put("commands.aborted", hc.aborted); snap.put("wakes.timed", wakes.timed); snap.put("wakes.device", wakes.device); snap.put("wakes.empty", wakes.empty); - snap.put("wakes.client", wakes.client); + let now = usb.wake.now(); snap.put("deadline_ns", wake_at.map_or("none".to_string(), |at| at.saturating_sub(now).to_string())); + let hid = usb.hid.get(); + snap.put("hid.reserved", hid.reserved); + snap.put("hid.unlisted", hid.unlisted); + snap.put("hid.unsent_button", hid.unsent_button); + snap.put("hid.unbound", hid.unbound); + let input = usb.input.borrow().counts; + snap.put("input.sent", input.sent); + snap.put("input.coalesced", input.coalesced); + snap.put("input.unowned", input.unowned); + snap.put("input.opened", input.opened); let encoded = snap.encode().unwrap_or_else(|why| panic!("usbd: its snapshot: {why}")); if let Err(why) = conn.try_send_bytes(MSG_SNAPSHOT, &encoded) { println!("usbd: dropping client {} — its answer would not go: {why:?}", conn.as_handle().0); diff --git a/system/usbd/xhci/Cargo.toml b/system/usbd/xhci/Cargo.toml new file mode 100644 index 00000000000..f57bad9ad91 --- /dev/null +++ b/system/usbd/xhci/Cargo.toml @@ -0,0 +1,23 @@ +# A member of the host workspace (root `Cargo.toml`): usbd depends on it by +# path, and its tests drive it against `src/stub/` on the host. + +[package] +name = "toyos-xhci" +description = "Every decision usbd makes about one xHCI controller and the devices on it, against the device-memory boundary, and none of the instructions that carry them out." +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +toyos-device-memory = { path = "../../../lib/device-memory" } +# The command a Bulk-Only wrapper carries, and what its answers mean. +toyos-scsi = { path = "../../../lib/scsi" } +# HID report descriptors and reports, and the owner of input's records. +toyos-usbhid = { path = "../../../lib/usbhid" } +toyos-input = { path = "../../../lib/input" } +toyos-abi = { path = "../../../sdk/abi" } +# A disk's partition table, read by the caller's reader over this crate's +# sectors; a session request's most blocks. +toyos-gpt = { path = "../../../lib/gpt" } +toyos-blockring = { path = "../../../lib/blockring" } diff --git a/lib/xhci/src/bot.rs b/system/usbd/xhci/src/bot.rs similarity index 90% rename from lib/xhci/src/bot.rs rename to system/usbd/xhci/src/bot.rs index b1bbbb10b72..00b3418553c 100644 --- a/lib/xhci/src/bot.rs +++ b/system/usbd/xhci/src/bot.rs @@ -7,9 +7,8 @@ //! told the device nothing, so which phase it stopped in is the only thing that //! says what the device is holding. //! -//! The phases are here and their effects are the driver's, because the reader -//! is the reset path: it runs where no lock may be taken, so it reads the phase -//! out of an atomic rather than out of the driver. +//! Which phase a break left the device in decides the ladder's first rung +//! (`ladder::left`). //! //! [`RoundTrip`] is the round trip as a machine the driver answers: which //! transfer is owed, what each completion means, the one legal stall retry, and @@ -17,18 +16,30 @@ //! touches a ring, so a driver that waits in place and one that gives the CPU //! back between transfers drive the same order. -use crate::job::{CC_SHORT_PACKET, CC_STALL, CC_SUCCESS}; +use crate::hc::{CC_SHORT_PACKET, CC_STALL, CC_SUCCESS}; use crate::ladder::{self, Left}; -use crate::reset_recovery::Pipe; use toyos_scsi::Cdb; -/// Where one Bulk-Only round trip stands, published by the driver as it walks -/// the three phases. -/// -/// **Five and not three**, though two pairs leave the device the same thing: -/// which of a pair a stopped machine is in is the difference between a device -/// the controller was still serving and one nothing had asked yet, and the -/// reset's account is the only place that is ever said. +/// One of a device's two bulk endpoints. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Pipe { + In, + Out, +} + +impl Pipe { + /// The pipe a transfer in this direction runs on. + pub fn of(device_to_host: bool) -> Self { + if device_to_host { + Self::In + } else { + Self::Out + } + } +} + +/// Where one Bulk-Only round trip stands: which of its transfers is queued +/// and unanswered, or owed and not yet queued. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] pub enum Phase { /// No command is open. Between round trips, and the state a device is left @@ -50,40 +61,6 @@ pub enum Phase { } impl Phase { - /// The byte the driver publishes and the reset path reads back. - /// - /// A number of this module's own and not a discriminant cast, so a phase - /// added between two existing ones cannot renumber what a reader in another - /// crate is decoding. - pub fn code(self) -> u8 { - match self { - Self::Closed => 0, - Self::Command => 1, - Self::DataOwed => 2, - Self::Data => 3, - Self::StatusOwed => 4, - Self::Status => 5, - } - } - - /// The phase `code` names, or [`Self::Closed`] for a byte no phase - /// publishes — the reset path may not panic on what it reads. - pub fn of(code: u8) -> Self { - match code { - 1 => Self::Command, - 2 => Self::DataOwed, - 3 => Self::Data, - 4 => Self::StatusOwed, - 5 => Self::Status, - _ => Self::Closed, - } - } - - /// Whether a device is inside a command here. - pub fn open(self) -> bool { - self != Self::Closed - } - /// What a line about this phase calls it — the driver's failure lines and /// the reset's account, so a reader never has to line two spellings up. pub fn named(self) -> &'static str { @@ -376,39 +353,6 @@ mod tests { Phase::Status, ]; - #[test] - fn every_phase_survives_the_byte_it_is_published_as() { - for phase in EVERY { - assert_eq!(Phase::of(phase.code()), phase); - } - for (at, phase) in EVERY.iter().enumerate() { - for other in &EVERY[at + 1..] { - assert_ne!( - phase.code(), - other.code(), - "{phase:?} and {other:?} publish the same byte" - ); - } - } - } - - #[test] - fn a_byte_no_phase_publishes_reads_as_closed() { - // The reader is the reset path, where a refusal that panicked would take - // the machine down inside the act that is handing it back. - for code in 6..=u8::MAX { - assert_eq!(Phase::of(code), Phase::Closed, "code {code}"); - } - } - - #[test] - fn every_phase_but_the_closed_one_is_a_device_holding_a_command() { - assert!(!Phase::Closed.open()); - for phase in EVERY.iter().copied().filter(|p| *p != Phase::Closed) { - assert!(phase.open(), "{phase:?}"); - } - } - #[test] fn no_two_phases_are_named_the_same_thing() { // The account names the phase and nothing else does; two phases with one diff --git a/lib/xhci/src/configure.rs b/system/usbd/xhci/src/configure.rs similarity index 62% rename from lib/xhci/src/configure.rs rename to system/usbd/xhci/src/configure.rs index 70a596ef902..818a5e410a0 100644 --- a/lib/xhci/src/configure.rs +++ b/system/usbd/xhci/src/configure.rs @@ -1,5 +1,6 @@ -//! The input context of the Configure Endpoint that creates a mass-storage -//! device's bulk pair, and of the one that re-creates it. +//! The input context of the Configure Endpoint that creates a device's +//! endpoints, and of the one that re-creates a mass-storage device's bulk +//! pair. //! //! Every dword the command reads is decided here and handed to the driver as a //! list it writes without looking inside, so there is no flag the driver can @@ -11,6 +12,8 @@ //! differ in the control context's Drop flags and in the slot context, and //! nowhere else. +use alloc::vec::Vec; + /// One dword of an input context: `context` 0 is the Input Control Context, 1 /// the Slot Context, `dci + 1` an endpoint's (xHCI 1.2 §6.2.5). #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -20,16 +23,28 @@ pub struct Word { pub value: u32, } -/// One bulk endpoint as its descriptor gave it, and the ring it starts on. +/// EP Type (Table 6-9). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Kind { + BulkOut = 2, + InterruptOut = 3, + BulkIn = 6, + InterruptIn = 7, +} + +/// One endpoint as its descriptor gave it, and the ring it starts on. #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct BulkEndpoint { +pub struct Endpoint { pub dci: u8, + pub kind: Kind, pub max_packet: u16, pub max_burst: u8, + /// The context's Interval exponent ([`interval`]); 0 for bulk. + pub interval: u8, pub dequeue: u64, } -/// Whether the command creates the pair or re-creates it. +/// Whether the command creates the endpoints or re-creates them. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Configure { /// The bind. The slot context names the speed the port trained at and the @@ -44,69 +59,79 @@ pub enum Configure { Again, } -/// How many dwords [`bulk_pair`] decides: two of the control context, four of -/// the slot context, five of each endpoint context. -pub const WORDS: usize = 16; - -/// EP Type (Table 6-9): 2 is Bulk Out, 6 is Bulk In. -const BULK_OUT: u32 = 2; -const BULK_IN: u32 = 6; /// CErr: three tries before a USB Transaction Error halts the endpoint. const CERR: u32 = 3; +/// The Interval exponent an interrupt endpoint's context carries for its +/// descriptor's `bInterval` at port speed `speed` (§6.2.3.6): frames at full +/// and low speed, read as 2^Interval × 125 µs rounded down and held to 3..=10; +/// an exponent plus one at high speed and above, held to 0..=15. +pub fn interval(speed: u8, b_interval: u8) -> u8 { + match speed { + 1 | 2 => { + let microframes = u32::from(b_interval.max(1)) * 8; + (31 - microframes.leading_zeros()).clamp(3, 10) as u8 + } + _ => b_interval.clamp(1, 16) - 1, + } +} + /// Every dword of the input context, in the order it is written. -pub fn bulk_pair(in_ep: BulkEndpoint, out_ep: BulkEndpoint, configure: Configure) -> [Word; WORDS] { - let endpoints = (1u32 << in_ep.dci) | (1u32 << out_ep.dci); - let entries = (in_ep.dci.max(out_ep.dci) as u32) << 27; +pub fn endpoints(eps: &[Endpoint], configure: Configure) -> Vec { + let flags = eps.iter().fold(0u32, |all, ep| all | 1 << ep.dci); + let entries = u32::from(eps.iter().map(|ep| ep.dci).max().unwrap_or(1)) << 27; let (drop, slot) = match configure { - Configure::First { speed, port } => { - (0, [((speed as u32) << 20) | entries, (port as u32) << 16, 0, 0]) - } - Configure::Again => (endpoints, [entries, 0, 0, 0]), + Configure::First { speed, port } => (0, [(u32::from(speed) << 20) | entries, u32::from(port) << 16, 0, 0]), + Configure::Again => (flags, [entries, 0, 0, 0]), }; let word = |context, dword, value| Word { context, dword, value }; - let endpoint = |ep: BulkEndpoint, ep_type: u32| { - let context = ep.dci as usize + 1; - [ - word(context, 0, 0), + // D0 and D1 stay clear, and A0 names the slot context every Configure + // Endpoint evaluates (§6.2.5.1). + let mut words = alloc::vec![ + word(0, 0, drop), + word(0, 1, 1 | flags), + word(1, 0, slot[0]), + word(1, 1, slot[1]), + word(1, 2, slot[2]), + word(1, 3, slot[3]), + ]; + for ep in eps { + let context = usize::from(ep.dci) + 1; + let payload = u32::from(ep.max_packet) * (u32::from(ep.max_burst) + 1); + let periodic = matches!(ep.kind, Kind::InterruptIn | Kind::InterruptOut); + words.extend([ + word(context, 0, u32::from(ep.interval) << 16), word( context, 1, - (CERR << 1) | (ep_type << 3) | ((ep.max_burst as u32) << 8) | ((ep.max_packet as u32) << 16), + (CERR << 1) | ((ep.kind as u32) << 3) | (u32::from(ep.max_burst) << 8) | (u32::from(ep.max_packet) << 16), ), word(context, 2, ep.dequeue as u32), word(context, 3, (ep.dequeue >> 32) as u32), - // Average TRB Length is advisory; the endpoint's own packet size - // stands in for it. - word(context, 4, ep.max_packet as u32), - ] - }; - let [o0, o1, o2, o3, o4] = endpoint(out_ep, BULK_OUT); - let [i0, i1, i2, i3, i4] = endpoint(in_ep, BULK_IN); - [ - // D0 and D1 stay clear, and A0 names the slot context every Configure - // Endpoint evaluates (§6.2.5.1). - word(0, 0, drop), - word(0, 1, 1 | endpoints), - word(1, 0, slot[0]), - word(1, 1, slot[1]), - word(1, 2, slot[2]), - word(1, 3, slot[3]), - o0, o1, o2, o3, o4, i0, i1, i2, i3, i4, - ] + // Average TRB Length is advisory: the packet size stands in for + // it. A periodic endpoint's Max ESIT Payload is what one service + // interval moves (§6.2.3.8). + word(context, 4, u32::from(ep.max_packet) | if periodic { payload << 16 } else { 0 }), + ]); + } + words } #[cfg(test)] mod tests { use super::*; - const IN: BulkEndpoint = - BulkEndpoint { dci: 3, max_packet: 512, max_burst: 0, dequeue: 0x1_2345_6001 }; - const OUT: BulkEndpoint = - BulkEndpoint { dci: 4, max_packet: 1024, max_burst: 15, dequeue: 0xABCD_E001 }; + const IN: Endpoint = + Endpoint { dci: 3, kind: Kind::BulkIn, max_packet: 512, max_burst: 0, interval: 0, dequeue: 0x1_2345_6001 }; + const OUT: Endpoint = + Endpoint { dci: 4, kind: Kind::BulkOut, max_packet: 1024, max_burst: 15, interval: 0, dequeue: 0xABCD_E001 }; const FIRST: Configure = Configure::First { speed: 4, port: 7 }; - fn read(words: &[Word; WORDS], context: usize, dword: usize) -> u32 { + fn bulk_pair(in_ep: Endpoint, out_ep: Endpoint, configure: Configure) -> Vec { + endpoints(&[out_ep, in_ep], configure) + } + + fn read(words: &[Word], context: usize, dword: usize) -> u32 { let mut found = words.iter().filter(|w| w.context == context && w.dword == dword); let word = found.next().unwrap_or_else(|| panic!("context {context} dword {dword} is never written")); assert!(found.next().is_none(), "context {context} dword {dword} is written twice"); @@ -164,7 +189,7 @@ mod tests { /// holds it. #[test] fn context_entries_is_the_higher_index_of_the_pair() { - let swapped = (BulkEndpoint { dci: 9, ..IN }, BulkEndpoint { dci: 2, ..OUT }); + let swapped = (Endpoint { dci: 9, ..IN }, Endpoint { dci: 2, ..OUT }); for configure in [FIRST, Configure::Again] { assert_eq!(read(&bulk_pair(swapped.0, swapped.1, configure), 1, 0) >> 27, 9); } @@ -204,4 +229,22 @@ mod tests { assert_eq!(read(&words, 4, 4), 512); assert_eq!(read(&words, 5, 4), 1024); } + + /// §6.2.3.6, against numbers worked by hand: a full-speed keyboard of + /// bInterval 10 ms is serviced every 2^6 × 125 µs = 8 ms, never slower + /// than asked; a high-speed one of bInterval 4 every 2^3 microframes. + #[test] + fn an_interrupt_endpoints_interval_is_the_specifications_exponent() { + assert_eq!(interval(1, 10), 6); + assert_eq!(interval(2, 1), 3); + assert_eq!(interval(1, 255), 10); + assert_eq!(interval(3, 4), 3); + assert_eq!(interval(4, 1), 0); + assert_eq!(interval(3, 0), 0); + let kb = Endpoint { dci: 3, kind: Kind::InterruptIn, max_packet: 8, max_burst: 0, interval: 6, dequeue: 0x1000 }; + let words = endpoints(&[kb], FIRST); + assert_eq!(read(&words, 4, 0), 6 << 16); + assert_eq!(read(&words, 4, 1), (8 << 16) | (7 << 3) | (3 << 1)); + assert_eq!(read(&words, 4, 4), (8 << 16) | 8); + } } diff --git a/lib/xhci/src/descriptor.rs b/system/usbd/xhci/src/descriptor.rs similarity index 54% rename from lib/xhci/src/descriptor.rs rename to system/usbd/xhci/src/descriptor.rs index d177c8dbd91..1d174654c83 100644 --- a/lib/xhci/src/descriptor.rs +++ b/system/usbd/xhci/src/descriptor.rs @@ -8,11 +8,20 @@ //! type that is not the one asked for are refused by name; nothing here //! panics or loops on a device's word. +use alloc::vec::Vec; + /// The device descriptor's type, and the configuration's and an interface's /// (USB 2.0 Table 9-5). const DEVICE: u8 = 1; const CONFIGURATION: u8 = 2; const INTERFACE: u8 = 4; +const ENDPOINT: u8 = 5; +/// HID 1.11 §6.2.1's HID descriptor, and its report descriptor's type. +const HID: u8 = 0x21; +pub const REPORT: u8 = 0x22; +/// USB 3.2 §9.6.7's SuperSpeed Endpoint Companion. +const COMPANION: u8 = 0x30; +const ENDPOINT_BYTES: usize = 7; /// A device descriptor's length (§9.6.1), and the eight bytes of it that every /// device can deliver before its EP0 packet size is known. @@ -23,9 +32,11 @@ pub const PREFIX_BYTES: usize = 8; const CONFIGURATION_BYTES: usize = 9; const INTERFACE_BYTES: usize = 9; -/// The interfaces one configuration may name here. A device offering more is -/// not refused: the rest are not named. +/// The interfaces one configuration may name here, and the endpoints one +/// interface may. A device offering more is not refused: the rest are not +/// named. pub const MAX_INTERFACES: usize = 8; +pub const MAX_ENDPOINTS: usize = 4; /// Why a device's answer is not the descriptor it was asked for. #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -58,6 +69,11 @@ pub struct Device { pub class: Class, pub vendor: u16, pub product: u16, + /// bcdDevice. + pub release: u16, + /// iSerialNumber: the string descriptor naming its serial, 0 for none. + pub serial: u8, + pub configurations: u8, } /// A class triple, the device's or an interface's (USB-IF class codes). @@ -82,60 +98,136 @@ pub fn device(bytes: &[u8]) -> Result { class: Class { class: bytes[4], subclass: bytes[5], protocol: bytes[6] }, vendor: le16(8), product: le16(10), + release: le16(12), + serial: bytes[16], + configurations: bytes[17], }) } -/// The interfaces a configuration names, at alternate setting 0, in the order -/// it names them. +/// One endpoint of an interface (USB 2.0 §9.6.6), with its SuperSpeed +/// companion's burst (USB 3.2 §9.6.7). #[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Interfaces { - found: [Class; MAX_INTERFACES], - count: usize, +pub struct Endpoint { + /// bEndpointAddress: the number, and bit 7 for IN. + pub address: u8, + /// bmAttributes' transfer type: 0 control, 1 isochronous, 2 bulk, 3 + /// interrupt. + pub kind: u8, + /// wMaxPacketSize's size, bits 10:0. + pub max_packet: u16, + pub interval: u8, + pub max_burst: u8, } -impl Interfaces { - pub fn iter(&self) -> impl Iterator + '_ { - self.found[..self.count].iter().copied() +pub const BULK: u8 = 2; +pub const INTERRUPT: u8 = 3; + +impl Endpoint { + pub fn is_in(&self) -> bool { + self.address & 0x80 != 0 + } + + /// Its Device Context Index (xHCI 1.2 §4.5.1): twice its number, and one + /// more for IN. + pub fn dci(&self) -> u8 { + (self.address & 0x0F) * 2 + u8::from(self.is_in()) } } -/// Walk a configuration descriptor's interfaces, over `bytes` and no further -/// than its `wTotalLength` says the configuration reaches. +/// One interface at alternate setting 0, and what follows it up to the next. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Interface { + pub number: u8, + pub class: Class, + pub endpoints: Vec, + /// A HID interface's report descriptor length (HID 1.11 §6.2.1). + pub report_bytes: Option, +} + +/// A configuration (§9.6.3) and its interfaces, in the order it names them. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Configuration { + /// bConfigurationValue, what SET_CONFIGURATION selects it by. + pub value: u8, + pub interfaces: Vec, +} + +/// Walk a configuration descriptor, over `bytes` and no further than its +/// `wTotalLength` says the configuration reaches. /// /// A walk that reaches the end of what arrived before `wTotalLength` is not /// refused: a device asked for fewer bytes than its total answers fewer, and -/// what arrived is still a prefix of the configuration. -pub fn interfaces(bytes: &[u8]) -> Result { +/// what arrived is still a prefix of the configuration. Only alternate +/// setting 0 is read: an alternate is the same interface again. +pub fn configuration(bytes: &[u8]) -> Result { header(bytes, CONFIGURATION, CONFIGURATION_BYTES)?; let total = u16::from_le_bytes([bytes[2], bytes[3]]); if usize::from(total) < CONFIGURATION_BYTES { return Err(Refused::Total(total)); } let end = bytes.len().min(usize::from(total)); - let mut found = Interfaces { found: [Class { class: 0, subclass: 0, protocol: 0 }; MAX_INTERFACES], count: 0 }; + let mut found = Configuration { value: bytes[5], interfaces: Vec::new() }; + // Whether what follows belongs to an interface being read. + let mut reading = false; let mut at = usize::from(bytes[0]); // Each step moves at least two bytes, so the walk ends within `end / 2`. while at + 2 <= end { - let (len, kind) = (bytes[at], bytes[at + 1]); + let (len, kind) = (usize::from(bytes[at]), bytes[at + 1]); if len < 2 { - return Err(Refused::Length { at, says: len }); + return Err(Refused::Length { at, says: bytes[at] }); + } + let need = match kind { + INTERFACE => INTERFACE_BYTES, + ENDPOINT => ENDPOINT_BYTES, + _ => 2, + }; + if len < need { + return Err(Refused::Length { at, says: bytes[at] }); + } + // Cut short by what arrived: a prefix names what it holds whole. + if at + len.max(need) > end { + break; } - if kind == INTERFACE { - if usize::from(len) < INTERFACE_BYTES { - return Err(Refused::Length { at, says: len }); + let d = &bytes[at..at + len]; + match kind { + INTERFACE => { + reading = d[3] == 0 && found.interfaces.len() < MAX_INTERFACES; + if reading { + found.interfaces.push(Interface { + number: d[2], + class: Class { class: d[5], subclass: d[6], protocol: d[7] }, + endpoints: Vec::new(), + report_bytes: None, + }); + } } - // Cut short by what arrived: a prefix names what it holds whole. - if at + INTERFACE_BYTES > end { - break; + ENDPOINT if reading => { + let interface = found.interfaces.last_mut().expect("reading one"); + if interface.endpoints.len() < MAX_ENDPOINTS { + interface.endpoints.push(Endpoint { + address: d[2], + kind: d[3] & 0x3, + max_packet: u16::from_le_bytes([d[4], d[5]]) & 0x7FF, + interval: d[6], + max_burst: 0, + }); + } } - let alternate = bytes[at + 3]; - if alternate == 0 && found.count < MAX_INTERFACES { - found.found[found.count] = - Class { class: bytes[at + 5], subclass: bytes[at + 6], protocol: bytes[at + 7] }; - found.count += 1; + COMPANION if reading && len >= 3 => { + if let Some(endpoint) = found.interfaces.last_mut().and_then(|i| i.endpoints.last_mut()) { + endpoint.max_burst = d[2].min(15); + } } + HID if reading && len >= 9 => { + // bNumDescriptors at 5, then (type, length) pairs from 6. + let report = d[6..].as_chunks::<3>().0.iter().take(usize::from(d[5])).find(|c| c[0] == REPORT); + if let Some(c) = report { + found.interfaces.last_mut().expect("reading one").report_bytes = Some(u16::from_le_bytes([c[1], c[2]])); + } + } + _ => {} } - at += usize::from(len); + at += len; } Ok(found) } @@ -184,11 +276,14 @@ mod tests { #[test] fn a_configuration_names_its_interfaces_and_steps_over_its_endpoints() { - let found = interfaces(&QEMU_STORAGE_CONFIG).unwrap(); - let mut named = found.iter(); - let msc = Class { class: 8, subclass: 6, protocol: 0x50 }; - assert_eq!(named.next(), Some(msc)); - assert_eq!(named.next(), None); + let found = configuration(&QEMU_STORAGE_CONFIG).unwrap(); + assert_eq!(found.value, 1); + let [msc] = &found.interfaces[..] else { panic!("{found:?}") }; + assert_eq!(msc.class, Class { class: 8, subclass: 6, protocol: 0x50 }); + let bulk = |address| Endpoint { address, kind: BULK, max_packet: 512, interval: 0, max_burst: 0 }; + assert_eq!(msc.endpoints, [bulk(0x81), bulk(0x02)]); + assert_eq!((msc.endpoints[0].dci(), msc.endpoints[1].dci()), (3, 4)); + assert_eq!(msc.report_bytes, None); } #[test] @@ -196,7 +291,7 @@ mod tests { assert_eq!(device(&QEMU_STORAGE[..17]), Err(Refused::Short { arrived: 17, need: 18 })); assert_eq!(ep0_packet(&QEMU_STORAGE[..7]), Err(Refused::Short { arrived: 7, need: 8 })); assert_eq!(device(&QEMU_STORAGE_CONFIG[..18]), Err(Refused::Type { want: 1, got: 2 })); - assert_eq!(interfaces(&QEMU_STORAGE), Err(Refused::Type { want: 2, got: 1 })); + assert_eq!(configuration(&QEMU_STORAGE), Err(Refused::Type { want: 2, got: 1 })); let mut short = QEMU_STORAGE; short[0] = 17; assert_eq!(device(&short), Err(Refused::Length { at: 0, says: 17 })); @@ -209,14 +304,14 @@ mod tests { for says in [0u8, 1] { let mut bad = QEMU_STORAGE_CONFIG; bad[18] = says; - assert_eq!(interfaces(&bad), Err(Refused::Length { at: 18, says })); + assert_eq!(configuration(&bad), Err(Refused::Length { at: 18, says })); } let mut bad = QEMU_STORAGE_CONFIG; bad[9] = 8; - assert_eq!(interfaces(&bad), Err(Refused::Length { at: 9, says: 8 })); + assert_eq!(configuration(&bad), Err(Refused::Length { at: 9, says: 8 })); let mut bad = QEMU_STORAGE_CONFIG; bad[2] = 8; - assert_eq!(interfaces(&bad), Err(Refused::Total(8))); + assert_eq!(configuration(&bad), Err(Refused::Total(8))); } /// The walk stops at `wTotalLength` whatever else arrived, and at what @@ -226,11 +321,11 @@ mod tests { let mut two = [0u8; 41]; two[..32].copy_from_slice(&QEMU_STORAGE_CONFIG); two[32..].copy_from_slice(&[9, 4, 1, 0, 0, 0x03, 0x01, 0x01, 0]); - assert_eq!(interfaces(&two).unwrap().iter().count(), 1, "past wTotalLength"); + assert_eq!(configuration(&two).unwrap().interfaces.len(), 1, "past wTotalLength"); two[2] = 41; - assert_eq!(interfaces(&two).unwrap().iter().count(), 2); - assert_eq!(interfaces(&two[..40]).unwrap().iter().count(), 1, "cut short by what arrived"); - assert_eq!(interfaces(&QEMU_STORAGE_CONFIG[..9]).unwrap().iter().count(), 0); + assert_eq!(configuration(&two).unwrap().interfaces.len(), 2); + assert_eq!(configuration(&two[..40]).unwrap().interfaces.len(), 1, "cut short by what arrived"); + assert_eq!(configuration(&QEMU_STORAGE_CONFIG[..9]).unwrap().interfaces.len(), 0); } /// An alternate setting is the same interface again, and is not named twice. @@ -240,7 +335,25 @@ mod tests { alt[..18].copy_from_slice(&QEMU_STORAGE_CONFIG[..18]); alt[18..].copy_from_slice(&[9, 4, 0, 1, 0, 0x08, 0x06, 0x50, 0]); alt[2] = 27; - assert_eq!(interfaces(&alt).unwrap().iter().count(), 1); + assert_eq!(configuration(&alt).unwrap().interfaces.len(), 1); + } + + /// A HID interface's own descriptor names its report descriptor's length, + /// and a SuperSpeed companion its endpoint's burst. + #[test] + fn a_hid_interface_names_its_report_length_and_a_companion_its_burst() { + let bytes = [ + 9, 2, 41, 0, 1, 1, 0, 0xa0, 50, // + 9, 4, 0, 0, 1, 0x03, 0x01, 0x01, 0, // + 9, 0x21, 0x11, 0x01, 0, 1, 0x22, 63, 0, // + 7, 5, 0x81, 3, 8, 0, 10, // + 6, 0x30, 4, 0, 0, 0, // + 3, 0x99, 0, + ]; + let found = configuration(&bytes).unwrap(); + let hid = &found.interfaces[0]; + assert_eq!(hid.report_bytes, Some(63)); + assert_eq!(hid.endpoints, [Endpoint { address: 0x81, kind: INTERRUPT, max_packet: 8, interval: 10, max_burst: 4 }]); } /// Every arrangement of a short buffer is refused or walked, never a panic: @@ -267,7 +380,7 @@ mod tests { } let _ = device(&bytes[..len]); let _ = ep0_packet(&bytes[..len]); - let _ = interfaces(&bytes[..len]); + let _ = configuration(&bytes[..len]); } } } diff --git a/system/usbd/xhci/src/exec.rs b/system/usbd/xhci/src/exec.rs new file mode 100644 index 00000000000..0effbc2f2fc --- /dev/null +++ b/system/usbd/xhci/src/exec.rs @@ -0,0 +1,232 @@ +//! The one thread usbd runs on, as tasks that each wait on one thing. +//! +//! **A task waits only at a leaf**: [`until`], which is ready when its +//! condition holds, or with a deadline when the clock passes it. A task's own +//! code runs only between a leaf that came ready and the next one it waits +//! at, so a round of [`Tasks::run`] in which no leaf came ready changed +//! nothing, and the next round would be the same: that is where a run stops, +//! with the earliest deadline any leaf asked for. So nothing spins, and +//! nothing is missed between two wakes. +//! +//! Nothing here allocates per poll, and no waker is ever woken: every task is +//! polled every round, which is cheap at a root hub's handful of tasks. + +use alloc::boxed::Box; +use alloc::vec::Vec; +use core::cell::Cell; +use core::future::{poll_fn, Future}; +use core::pin::Pin; +use core::task::{Context, Poll, Waker}; + +/// Nanoseconds since boot, as the caller's clock counts them. +pub type Nanos = u64; + +/// What usbd's process gives the driver: the clock its deadlines are on, one +/// counter read, and where its lines go. +pub trait Host { + fn now(&self) -> Nanos; + fn say(&self, line: core::fmt::Arguments<'_>); +} + +/// What every leaf reports to: the clock, the earliest deadline asked for, +/// and whether any leaf came ready this round. +pub struct Wake { + clock: C, + at: Cell>, + moved: Cell, +} + +impl Wake { + pub fn new(clock: C) -> Self { + Self { clock, at: Cell::new(None), moved: Cell::new(false) } + } + + pub fn now(&self) -> Nanos { + self.clock.now() + } + + pub fn host(&self) -> &C { + &self.clock + } + + fn deadline(&self, at: Nanos) { + self.at.set(Some(self.at.get().map_or(at, |was| was.min(at)))); + } + + /// Something a task waits on changed outside every task: the next run + /// polls them all. + pub fn stir(&self) { + self.moved.set(true); + } +} + +/// Ready with `f`'s answer once it has one. +pub fn until<'a, C: Host, T>(wake: &'a Wake, mut f: impl FnMut() -> Option + 'a) -> impl Future + 'a { + poll_fn(move |_| match f() { + Some(t) => { + wake.moved.set(true); + Poll::Ready(t) + } + None => Poll::Pending, + }) +} + +/// Ready with `f`'s answer once it has one, or with `None` once the clock +/// reaches `at`, whichever is first. +pub fn until_or<'a, C: Host, T>( + wake: &'a Wake, + at: Nanos, + mut f: impl FnMut() -> Option + 'a, +) -> impl Future> + 'a { + poll_fn(move |_| { + if let Some(t) = f() { + wake.moved.set(true); + return Poll::Ready(Some(t)); + } + if wake.now() >= at { + wake.moved.set(true); + return Poll::Ready(None); + } + wake.deadline(at); + Poll::Pending + }) +} + +/// Ready once the clock reaches `at`. +pub async fn sleep_until(wake: &Wake, at: Nanos) { + until_or(wake, at, || None::<()>).await; +} + +/// A task, or one function of a device's: it ends with the driver's death or +/// its own. +pub type Task<'a> = Pin> + 'a>>; + +/// Every one of `all`, run together; ready once each is, or with the first +/// that ended the driver. +pub async fn join(mut all: Vec>) -> Result<(), Dead> { + poll_fn(move |cx| { + let mut i = 0; + while i < all.len() { + match all[i].as_mut().poll(cx) { + Poll::Pending => i += 1, + Poll::Ready(Ok(())) => drop(all.remove(i)), + Poll::Ready(Err(dead)) => return Poll::Ready(Err(dead)), + } + } + if all.is_empty() { Poll::Ready(Ok(())) } else { Poll::Pending } + }) + .await +} + +impl Wake { + /// Ask the run to end no later than `at`: for a hand-written leaf. + pub fn wake_at(&self, at: Nanos) { + self.deadline(at); + } +} + +/// Why the driver stopped: the controller no longer answers, and nothing +/// here can take back what it holds. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Dead(pub &'static str); + + +/// Every task, polled in the order spawned. +#[derive(Default)] +pub struct Tasks<'a> { + tasks: Vec>, +} + +/// Rounds one run may take before a task is called a spin: every round but +/// the last moved something, and no sequence in this driver moves that often +/// without waiting. +const ROUNDS: usize = 100_000; + +impl<'a> Tasks<'a> { + pub fn spawn(&mut self, task: impl Future> + 'a) { + self.tasks.push(Box::pin(task)); + } + + /// Poll every task until a round moves nothing; answer the earliest + /// deadline a leaf is waiting on, or why the driver is dead. + pub fn run(&mut self, wake: &Wake) -> Result, Dead> { + let mut cx = Context::from_waker(Waker::noop()); + for _ in 0..ROUNDS { + wake.moved.set(false); + wake.at.set(None); + let mut i = 0; + while i < self.tasks.len() { + match self.tasks[i].as_mut().poll(&mut cx) { + Poll::Pending => i += 1, + Poll::Ready(Ok(())) => { + drop(self.tasks.remove(i)); + wake.moved.set(true); + } + Poll::Ready(Err(dead)) => return Err(dead), + } + } + if !wake.moved.get() { + return Ok(wake.at.get()); + } + } + panic!("usbd: {ROUNDS} rounds in one run each moved something: a task is spinning"); + } + + pub fn len(&self) -> usize { + self.tasks.len() + } + + pub fn is_empty(&self) -> bool { + self.tasks.is_empty() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::cell::Cell; + + struct Fake(Cell); + impl Host for &Fake { + fn now(&self) -> Nanos { + self.0.get() + } + fn say(&self, _: core::fmt::Arguments<'_>) {} + } + + /// A task that waits on another's write runs in the same run, and a run + /// with nothing ready answers the deadline asked for. + #[test] + fn a_run_ends_when_nothing_moves_and_answers_the_earliest_deadline() { + let clock = Fake(Cell::new(0)); + let wake = Wake::new(&clock); + let flag = Cell::new(false); + let seen = Cell::new(0); + let mut tasks = Tasks::default(); + tasks.spawn(async { + until(&wake, || seen.get().eq(&1).then_some(())).await; + seen.set(2); + Ok(()) + }); + tasks.spawn(async { + until(&wake, || flag.get().then_some(())).await; + seen.set(1); + sleep_until(&wake, 50).await; + seen.set(3); + Ok(()) + }); + tasks.spawn(async { + sleep_until(&wake, 20).await; + Ok(()) + }); + assert_eq!(tasks.run(&wake), Ok(Some(20))); + flag.set(true); + assert_eq!(tasks.run(&wake), Ok(Some(20))); + assert_eq!(seen.get(), 2, "the waiter on the setter's write ran in the setter's run"); + clock.0.set(20); + assert_eq!(tasks.run(&wake), Ok(Some(50))); + clock.0.set(50); + assert_eq!(tasks.run(&wake), Ok(None)); + assert_eq!((seen.get(), tasks.len()), (3, 0)); + } +} diff --git a/lib/xhci/src/flush.rs b/system/usbd/xhci/src/flush.rs similarity index 100% rename from lib/xhci/src/flush.rs rename to system/usbd/xhci/src/flush.rs diff --git a/system/usbd/xhci/src/hc.rs b/system/usbd/xhci/src/hc.rs new file mode 100644 index 00000000000..cbf2937fb7c --- /dev/null +++ b/system/usbd/xhci/src/hc.rs @@ -0,0 +1,1161 @@ +//! The controller: its register file, the one DMA grant every ring and +//! context lives in, and every operation it owes an answer for. +//! +//! What is here is xHCI 1.2's register file and memory layout, written once: +//! the §4.22.1 handoff, the §4.2 bring-up, the command ring (§4.6), the event +//! ring (§4.9.4), the transfer rings and the doorbells. Nothing here decides +//! anything about a port or a device. +//! +//! **An operation is matched by the TRB its event names** (§6.4.2.1, §6.4.2.2): +//! the only name an event carries that belongs to one operation and no other. +//! [`Op`] is one the controller holds. **A deadline is never a cancellation**: +//! dropping an `Op` the controller has not answered is a panic by name, and +//! what takes one back is the command the specification gives for it +//! ([`Hc::command`]'s Command Abort, §4.6.1.2; a transfer's Stop Endpoint and +//! Set TR Dequeue Pointer, §4.6.9 and §4.6.10, in the caller). +//! +//! **The barriers are the grant's** (`toyos-device-memory`): a TRB's body is +//! stored, then `publish`, then its control word with the cycle bit, then +//! `publish` again before the doorbell; an event's cycle bit is loaded, then +//! `observe`, then the rest of it. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; +use core::cell::{Cell, RefCell}; +use core::num::NonZeroU16; + +use toyos_device_memory::{DmaBuffers, Registers}; + +use crate::exec::{self, Dead, Host, Nanos, Wake}; +use crate::portsc::{Portsc, Write}; +use crate::protocol::{Protocols, SupportedProtocol}; +use crate::xecp::{self, Handoff}; + +const CAP_CAPLENGTH: usize = 0x00; +const CAP_HCSPARAMS1: usize = 0x04; +const CAP_HCSPARAMS2: usize = 0x08; +const CAP_HCCPARAMS1: usize = 0x10; +const CAP_DBOFF: usize = 0x14; +const CAP_RTSOFF: usize = 0x18; +/// HCCPARAMS1's 64-bit Addressing Capability and Context Size (§5.3.6). +const HCCPARAMS1_AC64: u32 = 1 << 0; +const HCCPARAMS1_CSZ: u32 = 1 << 2; + +const OP_USBCMD: usize = 0x00; +const OP_USBSTS: usize = 0x04; +const OP_PAGESIZE: usize = 0x08; +const OP_CRCR: usize = 0x18; +const OP_DCBAAP: usize = 0x30; +const OP_CONFIG: usize = 0x38; +const OP_PORT_BASE: usize = 0x400; +const PORT_REG_SIZE: usize = 0x10; +/// PORTSC's Port Power (§5.4.8). +const PORTSC_PP: u32 = 1 << 9; + +/// USBCMD's Run/Stop, Host Controller Reset and Interrupter Enable; USBSTS's +/// HCHalted and Controller Not Ready (§5.4.1, §5.4.2). +const USBCMD_RS: u32 = 1 << 0; +const USBCMD_HCRST: u32 = 1 << 1; +const USBCMD_INTE: u32 = 1 << 2; +const USBSTS_HCH: u32 = 1 << 0; +const USBSTS_CNR: u32 = 1 << 11; +/// CRCR's Command Abort (§5.4.5). +const CRCR_CA: u32 = 1 << 2; + +/// Interrupter 0's registers in the runtime space (§5.5.2). +const IR0_IMAN: usize = 0x20; +const IR0_IMOD: usize = 0x24; +const IR0_ERSTSZ: usize = 0x28; +const IR0_ERSTBA: usize = 0x30; +const IR0_ERDP: usize = 0x38; +/// IMAN's Interrupt Pending (write 1 to clear) and Interrupt Enable. +const IMAN_IP_IE: u32 = 0b11; +/// ERDP's Event Handler Busy, written 1 to clear (§5.5.2.3.3). +const ERDP_EHB: u64 = 1 << 3; + +const TRB_CYCLE: u32 = 1; +const fn trb_type(t: u32) -> u32 { + t << 10 +} +pub const TRB_NORMAL: u32 = trb_type(1); +pub const TRB_SETUP: u32 = trb_type(2); +pub const TRB_DATA: u32 = trb_type(3); +pub const TRB_STATUS: u32 = trb_type(4); +const TRB_LINK: u32 = trb_type(6); +pub const TRB_ENABLE_SLOT: u32 = trb_type(9); +pub const TRB_DISABLE_SLOT: u32 = trb_type(10); +pub const TRB_ADDRESS_DEVICE: u32 = trb_type(11); +pub const TRB_CONFIGURE_ENDPOINT: u32 = trb_type(12); +pub const TRB_EVALUATE_CONTEXT: u32 = trb_type(13); +pub const TRB_RESET_ENDPOINT: u32 = trb_type(14); +pub const TRB_STOP_ENDPOINT: u32 = trb_type(15); +pub const TRB_SET_TR_DEQUEUE: u32 = trb_type(16); +pub const TRB_NO_OP: u32 = trb_type(23); +/// Link's Toggle Cycle; Interrupt-on Short Packet, Chain, Interrupt On +/// Completion and Immediate Data (§6.4.1). +const LINK_TC: u32 = 1 << 1; +pub const TRB_ISP: u32 = 1 << 2; +pub const TRB_CHAIN: u32 = 1 << 4; +pub const TRB_IOC: u32 = 1 << 5; +pub const TRB_IDT: u32 = 1 << 6; +/// A Data or Status stage's direction, device to host (§6.4.1.2). +pub const TRB_DIR_IN: u32 = 1 << 16; + +const EVENT_TRANSFER: u32 = 32; +const EVENT_COMMAND_COMPLETION: u32 = 33; +const EVENT_PORT_STATUS_CHANGE: u32 = 34; + +/// Completion codes (§6.4.5) this driver acts on. +pub const CC_SUCCESS: u32 = 1; +pub const CC_STALL: u32 = 6; +pub const CC_SHORT_PACKET: u32 = 13; +pub const CC_COMMAND_RING_STOPPED: u32 = 24; +pub const CC_COMMAND_ABORTED: u32 = 25; +pub const CC_STOPPED: u32 = 26; +pub const CC_STOPPED_LENGTH_INVALID: u32 = 27; + +/// The granularity everything in the grant is placed at; the controller's own +/// PAGESIZE must include it. +pub const PAGE: usize = 0x1000; +/// TRBs per ring: one page, its last a Link back to the first. +const RING_TRBS: usize = PAGE / 16; +/// The most bytes one TRB moves, and the boundary none may cross (§6.4.1). +pub const TRB_MOST: u64 = 0x1_0000; + +const OFF_DCBAA: usize = 0; +const OFF_CMD_RING: usize = PAGE; +const OFF_ERST: usize = 2 * PAGE; +const OFF_EVENT_RING: usize = 3 * PAGE; +const OFF_SCRATCH_ARRAY: usize = 4 * PAGE; +const OFF_SCRATCH: usize = 5 * PAGE; + +/// The devices this controller is given room for, whatever more slots it has. +pub const MAX_DEVICES: usize = 32; +/// Pages each device may hold at once: its contexts, its rings and its data. +pub const DEVICE_PAGES: usize = 12; +/// The scratchpad buffers one page of array can name. +const MAX_SCRATCH: usize = PAGE / 8; + +/// How long a register the controller sets in microseconds may take, read +/// without an interrupt, since the controller raises none for these bits +/// (§5.4.1, §5.4.2). Policy. +const REGISTER_NS: Nanos = 2_000_000_000; +/// How long firmware is given to let go of the controller: the OS sets HC OS +/// Owned and waits for HC BIOS Owned to clear (§4.22.1; USBLEGSUP, §7.1.1), +/// a wait §4.22.1 says "should not exceed '1' second". +const HANDOFF_NS: Nanos = 1_000_000_000; +/// How long a command may go unanswered before it is aborted, and an abort +/// before the controller is called dead: "an upper limit of 5 seconds for any +/// command to be processed" (USB 3.2 §9.2.6.1) bounds what a device makes a +/// command wait on. +pub const COMMAND_NS: Nanos = 5_000_000_000; + +/// One TRB, as the rings carry it (§4.11). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Trb { + pub param: u64, + pub status: u32, + pub control: u32, +} + +/// One TD of Normal TRBs moving `pieces` on an endpoint of `max_packet`: +/// every TRB but the last chains, the last interrupts, each interrupts on a +/// short packet, and each states the packets left after it (TD Size, §4.11.2.4). +pub fn normal_td(pieces: &[(u64, u32)], max_packet: NonZeroU16) -> Vec { + let mps = u64::from(max_packet.get()); + let packets = pieces.iter().map(|&(_, len)| u64::from(len)).sum::().div_ceil(mps); + let mut moved = 0u64; + pieces + .iter() + .enumerate() + .map(|(i, &(at, len))| { + moved += u64::from(len); + let last = i + 1 == pieces.len(); + let left = if last { 0 } else { (packets - moved / mps).min(31) as u32 }; + let flags = if last { TRB_IOC } else { TRB_CHAIN }; + Trb { param: at, status: left << 17 | len, control: TRB_NORMAL | TRB_ISP | flags } + }) + .collect() +} + +/// Why the controller was not brought up. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Refusal { + /// A register the controller published puts what it names outside the + /// window the claim maps. + Window(&'static str, usize), + /// A register bit that did not move within its bound. + Stuck(&'static str), + PageSize(u32), + Scratchpad(usize), + /// The grant is smaller than [`Reset::grant_bytes`]. + Grant(usize), + /// Firmware still held HC BIOS Owned after [`HANDOFF_NS`]. §4.22.1 has the + /// OS wait for it to clear "before attempting to use the xHC": firmware + /// may still be driving the controller, and two agents would each believe + /// they own it. The controller is left as found. + FirmwareKeepsIt(u32), +} + +impl core::fmt::Display for Refusal { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Window(what, at) => write!(f, "{what} at {at:#x} is outside the register window"), + Self::Stuck(what) => write!(f, "{what} within {} ms", REGISTER_NS / 1_000_000), + Self::PageSize(p) => write!(f, "PAGESIZE={p:#x} does not include 4 KiB, where every ring is placed"), + Self::Scratchpad(n) => write!(f, "it asks for {n} scratchpad buffers, past the {MAX_SCRATCH} one page names"), + Self::Grant(bytes) => write!(f, "a grant of {bytes} bytes is short of the layout"), + Self::FirmwareKeepsIt(legsup) => write!( + f, + "firmware still owns it after {} ms (USBLEGSUP {legsup:#010x}, HC BIOS Owned set), and \ + xHCI 1.2 §4.22.1 has the OS wait for that to clear before it uses the controller", + HANDOFF_NS / 1_000_000 + ), + } + } +} + +/// A ring this driver produces into: the command ring or a transfer ring. +#[derive(Clone, Copy, Debug)] +pub struct Ring { + /// The ring's page in the grant. + pub at: usize, + tail: usize, + cycle: bool, +} + +impl Ring { + fn new(dma: &D, at: usize) -> Self { + for off in (0..PAGE).step_by(8) { + dma.write64(at + off, 0); + } + let ring = Self { at, tail: 0, cycle: true }; + let base = dma.device_addr(at); + put(dma, at + (RING_TRBS - 1) * 16, Trb { param: base, status: 0, control: TRB_LINK | LINK_TC }); + ring + } + + /// Where the controller resumes, with the cycle state it must expect in + /// bit 0: what a context or a Set TR Dequeue names. + pub fn dequeue(&self, dma: &D) -> u64 { + dma.device_addr(self.at + self.tail * 16) | u64::from(self.cycle) + } + + /// Put `trbs`, one TD, on the ring with this ring's cycle bit, and answer + /// where each landed: the names their events will carry. + /// + /// **A TD never crosses the ring's Link**: one that would not fit before + /// it starts on the ring's first TRB, behind a Link TD put where it would + /// have begun (§4.11.7), so no TD Fragment spans a segment (§4.11.5.1, + /// §4.11.7.1). **Its first TRB is written last** (§4.11.7.1), so the + /// controller finds none of the TD before all of it. + fn enqueue(&mut self, dma: &D, trbs: &[Trb]) -> Vec { + assert!(!trbs.is_empty() && trbs.len() <= Self::room(), "a TD of {} TRBs on a ring that holds {}", trbs.len(), Self::room()); + if self.tail + trbs.len() > RING_TRBS - 1 { + self.link(dma); + } + let (first, cycle) = (self.at + self.tail * 16, self.cycle); + let owned = |trb: &Trb, cycle: bool| Trb { control: (trb.control & !TRB_CYCLE) | u32::from(cycle), ..*trb }; + let mut at = Vec::with_capacity(trbs.len()); + for (i, trb) in trbs.iter().enumerate() { + at.push(dma.device_addr(self.at + self.tail * 16)); + // The first is put as the controller's not yet, and given to it below. + put(dma, self.at + self.tail * 16, owned(trb, if i == 0 { !cycle } else { cycle })); + self.tail += 1; + } + if self.tail == RING_TRBS - 1 { + self.link(dma); + } + dma.publish(); + dma.write32(first + 12, owned(&trbs[0], cycle).control); + at + } + + /// A Link TD at the tail, back to the ring's first TRB: where the + /// controller next turns, its cycle state toggled. + /// + /// **Every slot of a lap carries that lap's cycle when it closes**, so + /// none matches the cycle the controller expects on the next (§4.9.2): a + /// Link put early also marks each slot it skips, the ring's own Link + /// included, with the closing lap's cycle, or a slot two laps old would + /// be the controller's once the next lap's tail rests on it. + fn link(&mut self, dma: &D) { + for skipped in self.tail + 1..RING_TRBS { + dma.write32(self.at + skipped * 16 + 12, u32::from(self.cycle)); + } + let control = TRB_LINK | LINK_TC | u32::from(self.cycle); + put(dma, self.at + self.tail * 16, Trb { param: dma.device_addr(self.at), status: 0, control }); + self.tail = 0; + self.cycle = !self.cycle; + } + + /// The most TRBs one TD may have. A ring holds one operation at a time + /// here, so nothing the controller has not consumed is overrun but an + /// early Link: put at a tail `t` because `t + n` would pass the ring's + /// Link, it must not be overwritten by the TD that follows it, and this + /// bound makes `t + n > RING_TRBS - 1` imply `n <= t`. + pub const fn room() -> usize { + (RING_TRBS - 1) / 2 + } +} + +/// The body first and the control word last, behind a release: the control +/// word carries the cycle bit, and the controller owns the TRB the moment +/// that bit matches its own (§4.9.2). +fn put(dma: &D, at: usize, trb: Trb) { + dma.write64(at, trb.param); + dma.write32(at + 8, trb.status); + dma.publish(); + dma.write32(at + 12, trb.control); +} + +/// How an operation ended. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Done { + /// The controller's completion code, the bytes a transfer moved, and for + /// a command its parameter (Enable Slot's slot). + Event { code: u32, moved: u32, slot: u8 }, + /// Its slot was disabled under it: the device left, and nothing will + /// answer it. + Gone, +} + +impl Done { + pub fn code(self) -> Option { + match self { + Self::Event { code, .. } => Some(code), + Self::Gone => None, + } + } + + pub fn succeeded(self) -> bool { + matches!(self, Self::Event { code: CC_SUCCESS | CC_SHORT_PACKET, .. }) + } +} + +/// An operation the controller holds. +struct Held { + slot: u8, + /// 0 for a command. + dci: u8, + /// Each TRB's address and the bytes it was asked to move. + trbs: Vec<(u64, u32)>, + /// A control transfer's data stage that came short still runs its status + /// stage (§4.10.1.1), which is what ends it. + control: bool, + moved: Option, + done: Option, +} + +/// What the controller counted, which `inspect` reads. +#[derive(Clone, Copy, Default, Debug)] +pub struct Counts { + pub events: u64, + /// Operations whose deadline passed with the controller still holding + /// them. + pub silent: u64, + /// Of them, ones whose answer was already on the event ring: an event + /// no interrupt announced. + pub unannounced: u64, + /// Events naming a TRB no operation holds. + pub stray: u64, + pub other: u64, + pub aborted: u64, +} + +/// A controller reset and not yet given its grant: firmware has let go of it, +/// HCRST has run, and nothing of the machine is reachable to it. +pub struct Reset { + regs: R, + /// The device addresses the controller issues: 64 bits, or 32 where + /// HCCPARAMS1.AC64 is clear (§5.3.6). + pub address_bits: u8, + op: usize, + rt: usize, + db: usize, + max_ports: u8, + slots: usize, + context_bytes: usize, + scratch: usize, + pub protocols: Protocols, +} + +/// How long the stop before a machine reset may take, all of it: the +/// devices' reset, the controller's halt and its own reset. A policy, below +/// the supervisor's wait for every disk driver (`toyos_quiesce::DRIVERS_MS`). +pub const STOP_NS: Nanos = 2_000_000_000; + +/// How often a register the stop waits on is read again: none of them raises +/// an event the stop could wait for. +const STOP_POLL_NS: Nanos = 1_000_000; + +/// What [`Hc::stop`] did, each step as read back from the controller. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Stopped { + /// Ports with a device connected, and those whose reset finished. + pub connected: u32, + pub reset: u32, + pub halted: bool, + /// HCRST cleared and the controller ready again. + pub cleared: bool, + pub ports: u32, + /// Ports reading Port Power off afterwards: a controller without Port + /// Power Control keeps them powered (§5.4.8). + pub unpowered: u32, +} + +impl Stopped { + /// Every connected device was reset, and the controller halted and reset. + pub fn whole(&self) -> bool { + self.reset == self.connected && self.halted && self.cleared + } +} + +impl core::fmt::Display for Stopped { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "{}/{} connected port(s) reset, halted={}, reset={}, {}/{} port(s) unpowered", + self.reset, self.connected, self.halted, self.cleared, self.unpowered, self.ports + ) + } +} + +/// The controller, running. +pub struct Hc { + regs: R, + pub dma: D, + op: usize, + rt: usize, + db: usize, + pub max_ports: u8, + pub context_bytes: usize, + pub protocols: Protocols, + cmd: RefCell, + /// One command at a time, so an abort concerns one. + commanding: Cell, + /// The Command Ring Stopped event an abort is answered by. + stopped: Cell, + event: Cell<(usize, bool)>, + held: RefCell>, + /// Which operation each TRB the controller holds belongs to. + named: RefCell>, + next: Cell, + /// The driver is ending with operations held: the controller is gone, or + /// a test is over. + pub ending: Cell, + changed: RefCell>, + free: RefCell>, + pub counts: Cell, +} + +impl Reset { + /// The §4.22.1 handoff and a reset: everything before the grant, so a + /// controller refused here never masters the bus. + pub fn new(regs: R, host: &C) -> Result { + let cap_length = usize::from(regs.read8(CAP_CAPLENGTH)); + let hcsparams1 = regs.read32(CAP_HCSPARAMS1); + let hcsparams2 = regs.read32(CAP_HCSPARAMS2); + let hccparams1 = regs.read32(CAP_HCCPARAMS1); + let db = (regs.read32(CAP_DBOFF) & !0x3) as usize; + let rt = (regs.read32(CAP_RTSOFF) & !0x1f) as usize; + let max_slots = (hcsparams1 & 0xff) as usize; + let max_ports = (hcsparams1 >> 24) as u8; + let context_bytes = if hccparams1 & HCCPARAMS1_CSZ != 0 { 64 } else { 32 }; + let scratch = ((((hcsparams2 >> 21) & 0x1f) << 5) | ((hcsparams2 >> 27) & 0x1f)) as usize; + let op = cap_length; + for (what, end) in [ + ("the last port register", op + OP_PORT_BASE + usize::from(max_ports) * PORT_REG_SIZE), + ("interrupter 0", rt + IR0_ERDP + 8), + ("the last doorbell", db + 4 * (max_slots + 1)), + ] { + if end > regs.bytes() { + return Err(Refusal::Window(what, end)); + } + } + let pagesize = regs.read32(op + OP_PAGESIZE) & 0xffff; + if pagesize & 1 == 0 { + return Err(Refusal::PageSize(pagesize)); + } + if scratch > MAX_SCRATCH { + return Err(Refusal::Scratchpad(scratch)); + } + let read = |at: u64| -> Option { + let at = usize::try_from(at).ok()?; + (at % 4 == 0 && at + 4 <= regs.bytes()).then(|| regs.read32(at)) + }; + take_from_firmware(®s, &read, hccparams1 >> 16, host)?; + let protocols = read_protocols(&read, hccparams1 >> 16, max_ports, host); + + let usbcmd = regs.read32(op + OP_USBCMD); + if usbcmd & USBCMD_RS != 0 { + regs.write32(op + OP_USBCMD, usbcmd & !USBCMD_RS); + } + settle(host, "it never halted", || regs.read32(op + OP_USBSTS) & USBSTS_HCH != 0)?; + regs.write32(op + OP_USBCMD, USBCMD_HCRST); + settle(host, "it held HCRST", || regs.read32(op + OP_USBCMD) & USBCMD_HCRST == 0)?; + settle(host, "it stayed Controller Not Ready", || regs.read32(op + OP_USBSTS) & USBSTS_CNR == 0)?; + let slots = max_slots.min(MAX_DEVICES); + let address_bits = if hccparams1 & HCCPARAMS1_AC64 != 0 { 64 } else { 32 }; + Ok(Self { regs, address_bits, op, rt, db, max_ports, slots, context_bytes, scratch, protocols }) + } + + /// The grant the layout needs: the fixed head, the scratchpad, and every + /// device's pages. + pub fn grant_bytes(&self) -> usize { + OFF_SCRATCH + self.scratch * PAGE + self.slots * DEVICE_PAGES * PAGE + } + + /// Give the controller its grant, its rings and interrupter 0, and run it + /// (§4.2). + pub fn start(self, dma: D, host: &C) -> Result, Refusal> { + let bytes = self.grant_bytes(); + let Self { regs, op, rt, db, max_ports, slots, context_bytes, scratch, protocols, .. } = self; + if dma.bytes() < bytes { + return Err(Refusal::Grant(dma.bytes())); + } + for off in (0..bytes).step_by(8) { + dma.write64(off, 0); + } + regs.write32(op + OP_CONFIG, slots as u32); + for i in 0..scratch { + dma.write64(OFF_SCRATCH_ARRAY + i * 8, dma.device_addr(OFF_SCRATCH + i * PAGE)); + } + // DCBAA[0] names the scratchpad array (§6.1). + dma.write64(OFF_DCBAA, if scratch > 0 { dma.device_addr(OFF_SCRATCH_ARRAY) } else { 0 }); + let cmd = Ring::new(&dma, OFF_CMD_RING); + // One segment, the whole event ring (§6.5), named before ERSTBA is. + dma.write64(OFF_ERST, dma.device_addr(OFF_EVENT_RING)); + dma.write32(OFF_ERST + 8, RING_TRBS as u32); + dma.publish(); + let write64 = |at: usize, v: u64| { + regs.write32(at, v as u32); + regs.write32(at + 4, (v >> 32) as u32); + }; + write64(op + OP_DCBAAP, dma.device_addr(OFF_DCBAA)); + write64(op + OP_CRCR, cmd.dequeue(&dma)); + regs.write32(rt + IR0_ERSTSZ, 1); + write64(rt + IR0_ERDP, dma.device_addr(OFF_EVENT_RING)); + write64(rt + IR0_ERSTBA, dma.device_addr(OFF_ERST)); + // No moderation: every event is an interrupt. + regs.write32(rt + IR0_IMOD, 0); + regs.write32(rt + IR0_IMAN, IMAN_IP_IE); + regs.write32(op + OP_USBCMD, USBCMD_RS | USBCMD_INTE); + settle(host, "it stayed halted after Run", || regs.read32(op + OP_USBSTS) & USBSTS_HCH == 0)?; + let first = OFF_SCRATCH + scratch * PAGE; + let free = (0..slots * DEVICE_PAGES).rev().map(|i| first + i * PAGE).collect(); + let hc = Hc { + regs, + dma, + op, + rt, + db, + max_ports, + context_bytes, + protocols, + cmd: RefCell::new(cmd), + commanding: Cell::new(false), + stopped: Cell::new(false), + event: Cell::new((0, true)), + held: RefCell::new(BTreeMap::new()), + named: RefCell::new(BTreeMap::new()), + next: Cell::new(0), + ending: Cell::new(false), + changed: RefCell::new(alloc::vec![false; usize::from(max_ports)]), + free: RefCell::new(free), + counts: Cell::new(Counts::default()), + }; + // Port Power Control leaves a port unpowered after HCRST, and an + // unpowered port reports nothing; without PPC the write does nothing. + for p in 0..max_ports { + let portsc = hc.portsc(p); + if portsc.raw() & PORTSC_PP == 0 { + hc.write_portsc(p, portsc.neutral().powered()); + } + } + Ok(hc) + } +} + +impl Hc { + /// The machine is about to reset or power off: hand every device back + /// with no command outstanding and leave the controller as firmware + /// would find it, within [`STOP_NS`]. + /// + /// **A platform reset is not a way to end a transfer.** It leaves VBUS up, + /// so a device mid-Bulk-Only command sees no power cycle either. A port + /// reset returns the device to its Default state (USB 2.0 §7.1.7.5) and + /// abandons whatever command it was inside, so it comes first, on every + /// connected port at once, while the controller still runs. A USB 2 device + /// then gets TRSTRCY before anything else happens on its bus. After that + /// the controller is halted (§5.4.1), reset, and its ports unpowered where + /// it has Port Power Control. Every operation still held is let go: the + /// driver ends here. + pub async fn stop(&self, wake: &Wake) -> Stopped { + self.ending.set(true); + let deadline = wake.now().saturating_add(STOP_NS); + let mut done = Stopped { ports: u32::from(self.max_ports), ..Stopped::default() }; + let mut usb2 = false; + let mut reset: Vec = Vec::new(); + for port in 0..self.max_ports { + let portsc = self.portsc(port); + if !portsc.connected() { + continue; + } + usb2 |= self.protocols.of(port) != Some(crate::protocol::Protocol::Usb3); + // An earlier reset's flag cleared with the write, so a flag read + // after it is this reset's (`Portsc::reset_finished`). + self.write_portsc(port, portsc.neutral().acknowledging_reset(portsc).resetting()); + reset.push(port); + } + done.connected = reset.len() as u32; + let finished = || reset.iter().filter(|&&p| self.portsc(p).reset_finished()).count() as u32; + polled(wake, deadline, || finished() == done.connected).await; + done.reset = finished(); + if usb2 && done.reset > 0 { + exec::sleep_until(wake, wake.now().saturating_add(crate::usb::RESET_RECOVERY_NS)).await; + } + let usbcmd = self.regs.read32(self.op + OP_USBCMD); + self.regs.write32(self.op + OP_USBCMD, usbcmd & !USBCMD_RS); + done.halted = polled(wake, deadline, || self.regs.read32(self.op + OP_USBSTS) & USBSTS_HCH != 0).await; + if done.halted { + self.regs.write32(self.op + OP_USBCMD, USBCMD_HCRST); + done.cleared = polled(wake, deadline, || { + self.regs.read32(self.op + OP_USBCMD) & USBCMD_HCRST == 0 + && self.regs.read32(self.op + OP_USBSTS) & USBSTS_CNR == 0 + }) + .await; + } + for port in 0..self.max_ports { + self.write_portsc(port, self.portsc(port).neutral().unpowered()); + if self.portsc(port).raw() & PORTSC_PP == 0 { + done.unpowered += 1; + } + } + done + } + + pub fn portsc(&self, port: u8) -> Portsc { + Portsc::from_raw(self.regs.read32(self.op + OP_PORT_BASE + usize::from(port) * PORT_REG_SIZE)) + } + + pub fn write_portsc(&self, port: u8, write: Write) { + self.regs.write32(self.op + OP_PORT_BASE + usize::from(port) * PORT_REG_SIZE, write.raw()); + } + + /// Whether a Port Status Change Event has named `port` since the last + /// call, taking it. + pub fn take_change(&self, port: u8) -> bool { + core::mem::take(&mut self.changed.borrow_mut()[usize::from(port)]) + } + + /// A page of the grant for a device, zeroed, or `None` with none left. + pub fn page(&self) -> Option { + let at = self.free.borrow_mut().pop()?; + for off in (0..PAGE).step_by(8) { + self.dma.write64(at + off, 0); + } + Some(at) + } + + /// Pages the controller no longer reaches, given back. + pub fn release(&self, pages: impl IntoIterator) { + self.free.borrow_mut().extend(pages); + } + + pub fn pages_free(&self) -> usize { + self.free.borrow().len() + } + + /// A fresh ring on the page at `at`. + pub fn ring(&self, at: usize) -> Ring { + Ring::new(&self.dma, at) + } + + /// Name `out`, a device's output context page, as `slot`'s (§6.1); `None` + /// takes the name away once the slot is disabled. + pub fn name_slot(&self, slot: u8, out: Option) { + let addr = out.map_or(0, |at| self.dma.device_addr(at)); + self.dma.write64(OFF_DCBAA + usize::from(slot) * 8, addr); + self.dma.publish(); + } + + /// Context `index` of the context page at `page`: 0 is an input context's + /// control context or an output context's slot context (§6.2.1, §6.2.5). + pub fn context(&self, page: usize, index: usize) -> usize { + page + index * self.context_bytes + } + + /// Take the events the ring holds, at most one ring's worth, and tell the + /// controller so: Interrupt Pending cleared before the drain, and one ERDP + /// write after it, which clears Event Handler Busy (§4.17.2, §5.5.2.3.3). Answers + /// whether there was any; [`Self::event_waiting`] says whether more came. + pub fn take_events(&self) -> bool { + // Interrupt Pending first: an event the controller writes after this + // sets it again and interrupts, where one cleared after the drain + // would be an event no interrupt announces (§4.17.2). + self.regs.write32(self.rt + IR0_IMAN, IMAN_IP_IE); + let mut took = false; + let (mut head, mut cycle) = self.event.get(); + for _ in 0..RING_TRBS { + let at = OFF_EVENT_RING + head * 16; + let control = self.dma.read32(at + 12); + if (control & TRB_CYCLE != 0) != cycle { + break; + } + self.dma.observe(); + let param = self.dma.read64(at); + let status = self.dma.read32(at + 8); + head += 1; + if head == RING_TRBS { + head = 0; + cycle = !cycle; + } + took = true; + self.dispatch(control, param, status); + } + self.event.set((head, cycle)); + // Every drain ends Event Handler Busy, one that found nothing too: an + // interrupt raised for an event an earlier drain already took leaves + // it set, and while it is set no event raises another (§4.17.2). + let erdp = self.dma.device_addr(OFF_EVENT_RING + head * 16) | ERDP_EHB; + self.regs.write32(self.rt + IR0_ERDP, erdp as u32); + self.regs.write32(self.rt + IR0_ERDP + 4, (erdp >> 32) as u32); + took + } + + /// Whether an event is on the ring that nothing has taken. + pub fn event_waiting(&self) -> bool { + let (head, cycle) = self.event.get(); + let control = self.dma.read32(OFF_EVENT_RING + head * 16 + 12); + (control & TRB_CYCLE != 0) == cycle + } + + fn dispatch(&self, control: u32, param: u64, status: u32) { + let mut counts = self.counts.get(); + counts.events += 1; + let code = status >> 24; + match (control >> 10) & 0x3f { + EVENT_PORT_STATUS_CHANGE => { + let port = usize::from((param >> 24) as u8); + match port.checked_sub(1).and_then(|p| self.changed.borrow_mut().get_mut(p).map(|c| *c = true)) { + Some(()) => {} + None => counts.stray += 1, + } + } + EVENT_COMMAND_COMPLETION if code == CC_COMMAND_RING_STOPPED => self.stopped.set(true), + EVENT_COMMAND_COMPLETION => { + let slot = (control >> 24) as u8; + if !self.answer(param & !0xf, code, 0, slot) { + counts.stray += 1; + } + } + EVENT_TRANSFER => { + if !self.answer(param & !0xf, code, status & 0x00ff_ffff, 0) { + counts.stray += 1; + } + } + _ => counts.other += 1, + } + self.counts.set(counts); + } + + /// The event naming TRB `trb`: its operation's answer, or part of it. + fn answer(&self, trb: u64, code: u32, residue: u32, slot: u8) -> bool { + let Some(&id) = self.named.borrow().get(&trb) else { return false }; + let mut held = self.held.borrow_mut(); + let op = held.get_mut(&id).expect("a named TRB's operation is held"); + let index = op.trbs.iter().position(|(at, _)| *at == trb).expect("named by its own TRBs"); + let before: u32 = op.trbs[..index].iter().map(|(_, len)| *len).sum(); + let asked = op.trbs[index].1; + // A residue past what was asked is the controller contradicting + // itself: nothing of that TRB moved. + let moved = before + asked.saturating_sub(residue.min(asked)); + let last = index + 1 == op.trbs.len(); + let ends = last || code != CC_SHORT_PACKET || !op.control; + if !ends { + op.moved = Some(moved); + return true; + } + let moved = match (code, op.moved) { + (CC_SUCCESS, Some(short)) if last => short, + (CC_SUCCESS, None) if last => op.trbs.iter().map(|(_, len)| *len).sum(), + _ => moved, + }; + op.done = Some(Done::Event { code, moved, slot }); + true + } + + fn hold(&self, slot: u8, dci: u8, trbs: Vec<(u64, u32)>, control: bool) -> u64 { + let id = self.next.get(); + self.next.set(id + 1); + let mut named = self.named.borrow_mut(); + for (at, _) in &trbs { + named.insert(*at, id); + } + self.held.borrow_mut().insert(id, Held { slot, dci, trbs, control, moved: None, done: None }); + id + } + + fn done(&self, id: u64) -> Option { + self.held.borrow().get(&id).and_then(|op| op.done) + } + + /// The operation is over and the controller holds none of its TRBs. + fn forget(&self, id: u64) -> Done { + let op = self.held.borrow_mut().remove(&id).expect("an operation is forgotten once"); + let mut named = self.named.borrow_mut(); + for (at, _) in &op.trbs { + named.remove(at); + } + op.done.expect("forgotten once answered") + } + + /// Every operation on `slot` is answered [`Done::Gone`]: the slot was + /// disabled, or the controller would not say it was, and nothing on it + /// will be answered now. + pub fn slot_gone(&self, slot: u8) { + for op in self.held.borrow_mut().values_mut().filter(|op| op.slot == slot && op.dci != 0) { + op.done.get_or_insert(Done::Gone); + } + } + + /// Put `trbs`, one transfer descriptor, on `ring` and ring the endpoint's + /// doorbell. The last TRB interrupts; every one before it chains. + pub fn transfer(&self, slot: u8, dci: u8, ring: &mut Ring, trbs: &[Trb], control: bool) -> Op<'_, R, D> { + let at = ring.enqueue(&self.dma, trbs); + let named = at + .into_iter() + .zip(trbs) + .map(|(at, trb)| { + // A Setup stage's length is its own eight bytes, which no data + // count includes. + let moves = match trb.control & (0x3f << 10) { + TRB_SETUP | TRB_STATUS => 0, + _ => trb.status & 0x1_ffff, + }; + (at, moves) + }) + .collect(); + self.dma.publish(); + self.regs.write32(self.db + 4 * usize::from(slot), u32::from(dci)); + Op { hc: self, id: Some(self.hold(slot, dci, named, control)) } + } + + /// Issue `trb` on the command ring and await its completion. **One + /// command at a time**: another waits its turn. One unanswered within + /// [`COMMAND_NS`] is aborted (§4.6.1.2) and answered + /// [`CC_COMMAND_ABORTED`]; an abort the controller does not answer is a + /// controller that is gone. + pub async fn command(&self, wake: &Wake, trb: Trb) -> Result { + exec::until(wake, || (!self.commanding.get()).then_some(())).await; + self.commanding.set(true); + let at = self.cmd.borrow_mut().enqueue(&self.dma, &[trb])[0]; + self.dma.publish(); + self.regs.write32(self.db, 0); + let mut op = Op { hc: self, id: Some(self.hold(0, 0, alloc::vec![(at, 0)], false)) }; + let deadline = wake.now() + COMMAND_NS; + let done = match op.answer(wake, deadline).await { + Some(done) => done, + None => { + let mut counts = self.counts.get(); + counts.aborted += 1; + self.counts.set(counts); + self.stopped.set(false); + // The pointer fields are ignored while the ring runs, so the + // low dword alone carries the abort. + self.regs.write32(self.op + OP_CRCR, CRCR_CA); + let deadline = wake.now() + COMMAND_NS; + let stopped = exec::until_or(wake, deadline, || self.stopped.get().then_some(())).await; + let id = op.id.take().expect("still held"); + if stopped.is_none() { + // What it holds is past taking back: the process ends, + // and its claim's release is the controller's reset. + self.ending.set(true); + return Err(Dead("a command and then its abort went unanswered")); + } + // §4.6.1.2: the command the abort stopped is answered Command + // Aborted, and one that finished first keeps its own answer; + // a stopped ring holds neither. + let aborted = Done::Event { code: CC_COMMAND_ABORTED, moved: 0, slot: 0 }; + self.held.borrow_mut().get_mut(&id).expect("held").done.get_or_insert(aborted); + self.forget(id) + } + }; + self.commanding.set(false); + Ok(done) + } +} + +/// An operation the controller holds until its answer arrives or its slot +/// is disabled. +pub struct Op<'h, R: Registers, D: DmaBuffers> { + hc: &'h Hc, + id: Option, +} + +impl Op<'_, R, D> { + /// Its answer, or `None` once the clock reaches `deadline` with the + /// controller still holding it. + pub async fn answer(&mut self, wake: &Wake, deadline: Nanos) -> Option { + let id = self.id.expect("an operation is answered once"); + let Some(done) = exec::until_or(wake, deadline, || self.hc.done(id)).await else { + let mut counts = self.hc.counts.get(); + counts.silent += 1; + counts.unannounced += u64::from(self.hc.event_waiting()); + self.hc.counts.set(counts); + return None; + }; + self.hc.forget(id); + self.id = None; + Some(done) + } + + /// Its answer, however long the device takes: an interrupt endpoint's + /// report comes when a key moves. + pub async fn wait(mut self, wake: &Wake) -> Done { + let (hc, id) = (self.hc, self.id.expect("an operation is answered once")); + exec::until(wake, || hc.done(id)).await; + self.id = None; + hc.forget(id) + } + + /// The controller gave it back unanswered: a Set TR Dequeue Pointer moved + /// its ring past it, or its slot was disabled. An event for it that + /// arrives after is counted stray. + pub fn reclaimed(mut self) -> Done { + let id = self.id.take().expect("held"); + let stopped = Done::Event { code: CC_STOPPED, moved: 0, slot: 0 }; + self.hc.held.borrow_mut().get_mut(&id).expect("held").done.get_or_insert(stopped); + self.hc.forget(id) + } +} + +impl Drop for Op<'_, R, D> { + fn drop(&mut self) { + if self.hc.ending.get() { + return; + } + #[cfg(test)] + if std::thread::panicking() { + return; + } + if let Some(id) = self.id { + panic!("usbd: operation {id}, which the controller still holds, was let go of unanswered"); + } + } +} + +/// §4.22.1: ask firmware for the controller (HC OS Owned, USBLEGSUP §7.1.1) +/// and give it [`HANDOFF_NS`] to clear HC BIOS Owned. Released or never +/// claimed, its SMIs are turned off and the controller is the driver's; +/// kept, it is refused by name and left as found, since §4.22.1 has the OS +/// wait for firmware to let go "before attempting to use the xHC". +fn take_from_firmware( + regs: &R, + read: &dyn Fn(u64) -> Option, + xecp_dwords: u32, + host: &C, +) -> Result<(), Refusal> { + let legsup = match xecp::find(read, xecp_dwords, xecp::CAP_ID_LEGACY) { + Ok(Some(at)) if read(at + xecp::LEGCTLSTS).is_some() => at as usize, + Ok(Some(at)) => { + host.say(format_args!("USB Legacy Support at {at:#x} runs past the register window — no handoff")); + return Ok(()); + } + Ok(None) => { + host.say(format_args!("no USB Legacy Support capability, nothing to hand over")); + return Ok(()); + } + Err(why) => { + host.say(format_args!("the extended capability list does not walk ({why:?}) — no handoff")); + return Ok(()); + } + }; + let before = regs.read32(legsup); + regs.write32(legsup, before | xecp::LEGSUP_OS_OWNED); + let asked = host.now(); + let mut now = regs.read32(legsup); + // Firmware answers in its own SMI handler and raises nothing a process + // can wait on, so its semaphore is read until it clears or the bound passes. + while now & xecp::LEGSUP_BIOS_OWNED != 0 && host.now() - asked < HANDOFF_NS { + core::hint::spin_loop(); + now = regs.read32(legsup); + } + let took = (host.now() - asked) / 1000; + match xecp::handoff(before, now) { + Handoff::NeverClaimed => host.say(format_args!("firmware did not claim the controller (USBLEGSUP {before:#010x})")), + Handoff::Released => host.say(format_args!( + "firmware released the controller in {took} us (USBLEGSUP {before:#010x} -> {now:#010x})" + )), + Handoff::Kept => return Err(Refusal::FirmwareKeepsIt(now)), + } + let ctl_at = legsup + xecp::LEGCTLSTS as usize; + let ctl = regs.read32(ctl_at); + regs.write32(ctl_at, xecp::smis_off(ctl)); + let after = regs.read32(ctl_at); + host.say(format_args!("USBLEGCTLSTS {ctl:#010x} -> {after:#010x} (SMI generation off)")); + Ok(()) +} + +/// What each port register speaks, from every Supported Protocol capability. +fn read_protocols(read: &dyn Fn(u64) -> Option, xecp_dwords: u32, max_ports: u8, host: &C) -> Protocols { + let mut protocols = Protocols::UNKNOWN; + let walked = xecp::for_each(read, xecp_dwords, xecp::CAP_ID_PROTOCOL, &mut |at| { + let (Some(dw0), Some(dw1), Some(dw2)) = (read(at), read(at + 4), read(at + 8)) else { + return host.say(format_args!("a Supported Protocol capability at {at:#x} runs past the register window")); + }; + match SupportedProtocol::decode(dw0, dw1, dw2, max_ports) { + Ok(found) => { + host.say(format_args!( + "USB {}.{:x} on ports {}..={}", + found.major, + found.minor >> 4, + found.first_port + 1, + found.first_port + found.port_count + )); + protocols.record(&found); + } + Err(why) => host.say(format_args!("a Supported Protocol capability at {at:#x} is unusable: {why:?}")), + } + }); + if let Err(why) = walked { + host.say(format_args!("the extended capability list does not walk: {why:?}")); + } + protocols +} + +/// Read until `done` or [`REGISTER_NS`]: the controller sets these bits in +/// microseconds and raises no interrupt for any of them (§5.4.1, §5.4.2). +/// Whether `done` came true by `deadline`, read every [`STOP_POLL_NS`]. +async fn polled(wake: &Wake, deadline: Nanos, done: impl Fn() -> bool) -> bool { + loop { + if done() { + return true; + } + let now = wake.now(); + if now >= deadline { + return false; + } + exec::sleep_until(wake, now.saturating_add(STOP_POLL_NS).min(deadline)).await; + } +} + +fn settle(host: &C, what: &'static str, done: impl Fn() -> bool) -> Result<(), Refusal> { + let began = host.now(); + while !done() { + if host.now() - began >= REGISTER_NS { + return Err(Refusal::Stuck(what)); + } + core::hint::spin_loop(); + } + Ok(()) +} + +#[cfg(test)] +mod ring_tests { + use super::*; + + /// One ring page, its device address its offset. + struct Page(RefCell>); + + impl DmaBuffers for Page { + fn bytes(&self) -> usize { + self.0.borrow().len() + } + fn device_addr(&self, at: usize) -> u64 { + at as u64 + } + fn read16(&self, at: usize) -> u16 { + u16::from_le_bytes(self.0.borrow()[at..at + 2].try_into().unwrap()) + } + fn read32(&self, at: usize) -> u32 { + u32::from_le_bytes(self.0.borrow()[at..at + 4].try_into().unwrap()) + } + fn read64(&self, at: usize) -> u64 { + u64::from_le_bytes(self.0.borrow()[at..at + 8].try_into().unwrap()) + } + fn write16(&self, at: usize, v: u16) { + self.0.borrow_mut()[at..at + 2].copy_from_slice(&v.to_le_bytes()); + } + fn write32(&self, at: usize, v: u32) { + self.0.borrow_mut()[at..at + 4].copy_from_slice(&v.to_le_bytes()); + } + fn write64(&self, at: usize, v: u64) { + self.0.borrow_mut()[at..at + 8].copy_from_slice(&v.to_le_bytes()); + } + fn publish(&self) {} + fn observe(&self) {} + } + + /// The controller's walk (§4.9.2): from its dequeue, take each TRB whose + /// cycle is its Consumer Cycle State, following Links and toggling at + /// one with TC, and stop at the first that is not its own. + fn walk(page: &Page, at: &mut u64, ccs: &mut bool) -> Vec { + let mut took = Vec::new(); + for _ in 0..4 * RING_TRBS { + let control = page.read32(*at as usize + 12); + if (control & TRB_CYCLE != 0) != *ccs { + return took; + } + if control & (0x3f << 10) == TRB_LINK { + *at = page.read64(*at as usize); + *ccs ^= control & LINK_TC != 0; + } else { + took.push(*at); + *at += 16; + } + } + panic!("the walk never stopped"); + } + + /// TDs of 1 to 3 TRBs over many laps, early Links leaving gaps at every + /// offset: after each, the controller takes exactly that TD and stops at + /// the tail, from where a Set TR Dequeue would also resume it. + #[test] + fn the_controller_takes_each_td_and_stops_at_the_tail() { + let page = Page(RefCell::new(alloc::vec![0; PAGE])); + let mut ring = Ring::new(&page, 0); + let (mut at, mut ccs) = (0u64, true); + let mut seed = 0x2545_f491_u32; + let mut early = 0; + // The reviewer's lap first: one TRB a TD to the end, then to 253 + // and a 3-TRB TD, then one TRB a TD to rest on the gap. + let lengths = core::iter::repeat_n(1, 255 + 253) + .chain([3]) + .chain(core::iter::repeat_n(1, 254)) + .chain(core::iter::from_fn(|| { + seed ^= seed << 13; + seed ^= seed >> 17; + seed ^= seed << 5; + Some([1, 1, 2, 3][seed as usize % 4]) + })) + .take(20_000); + for n in lengths { + if ring.tail + n > RING_TRBS - 1 && ring.tail < RING_TRBS - 1 { + early += 1; + } + let trbs: Vec = (0..n).map(|i| Trb { param: i as u64, status: 0, control: TRB_NORMAL }).collect(); + let named = ring.enqueue(&page, &trbs); + assert_eq!(walk(&page, &mut at, &mut ccs), named); + assert_eq!((at, ccs), (ring.dequeue(&page) & !1, ring.dequeue(&page) & 1 == 1)); + } + assert!(early > 20, "{early} early Links"); + } + + /// TDs as long as [`Ring::room`] allows, put behind an early Link at + /// every tail that forces one: none overwrites the Link before it. + #[test] + fn the_longest_td_leaves_the_early_link_before_it_whole() { + let page = Page(RefCell::new(alloc::vec![0; PAGE])); + let mut ring = Ring::new(&page, 0); + let (mut at, mut ccs) = (0u64, true); + for lead in 0..RING_TRBS { + for n in [lead % 7 + 1, Ring::room()] { + let trbs: Vec = (0..n).map(|i| Trb { param: i as u64, status: 0, control: TRB_NORMAL }).collect(); + let named = ring.enqueue(&page, &trbs); + assert_eq!(walk(&page, &mut at, &mut ccs), named, "a TD of {n}"); + } + } + } +} diff --git a/system/usbd/xhci/src/hid.rs b/system/usbd/xhci/src/hid.rs new file mode 100644 index 00000000000..0951c592485 --- /dev/null +++ b/system/usbd/xhci/src/hid.rs @@ -0,0 +1,177 @@ +//! A HID interface, bound by its report descriptor in report protocol, its +//! reports read through `toyos-usbhid` into the owner of input's records. +//! +//! **One decode path.** A boot-subclass interface is told `SET_PROTOCOL(Report)`: +//! "When initialized, all devices default to report protocol. However the +//! host should not make any assumptions about the device's state and should +//! set the desired protocol whenever initializing a device" (HID 1.11 §7.2.6), +//! and the request is "required only for boot devices" (§7.2). An interface +//! with a keyboard is told `SET_IDLE(0)` (§7.2.4), so a report comes only on a +//! change; one that stalls it reports at its polling rate, and an unchanged +//! report is no transition. +//! +//! **Each bound collection is one device to the owner**; one this reader +//! cannot bind costs only its own reports, counted and dropped. + +use alloc::vec::Vec; + +use toyos_device_memory::{DmaBuffers, Registers}; +use toyos_usbhid::descriptor::{self as hid_descriptor, Kind}; +use toyos_usbhid::report::{Counts, Hid, Read}; + +use crate::descriptor::{Interface, INTERRUPT, REPORT}; +use crate::exec::{Dead, Host}; +use crate::hc::{self, Done, Trb}; +use crate::input::Handle; +use crate::usb::{Device, Setup}; + +/// The class requests (HID 1.11 §7.2): SET_IDLE and SET_PROTOCOL, to the +/// interface. +const SET_IDLE: u8 = 0x0A; +const SET_PROTOCOL: u8 = 0x0B; +const CLASS_TO_INTERFACE: u8 = 0x21; +/// GET_DESCRIPTOR of a class descriptor, from the interface (§7.1). +const STANDARD_FROM_INTERFACE: u8 = 0x81; + +/// Refused reports said by name, the first few; the rest are counted. +const SAID: u32 = 4; + +pub async fn serve(dev: &Device<'_, R, D, C, T>, interface: Interface) -> Result<(), Dead> { + let usb = dev.usb; + let at = (dev.port + 1, interface.number); + let say = |line: core::fmt::Arguments<'_>| usb.say(format_args!("port {} interface {}: {line}", at.0, at.1)); + let Some(ep) = interface.endpoints.iter().copied().find(|e| e.kind == INTERRUPT && e.is_in()) else { + say(format_args!("HID with no interrupt IN endpoint; not bound")); + return Ok(()); + }; + let Some(length) = interface.report_bytes.filter(|n| usize::from(*n) <= hc::PAGE) else { + say(format_args!("HID whose report descriptor length {:?} is not one page or less; not bound", interface.report_bytes)); + return Ok(()); + }; + let index = u16::from(interface.number); + if interface.class.subclass == 1 { + let report = Setup { request_type: CLASS_TO_INTERFACE, request: SET_PROTOCOL, value: 1, index, length: 0 }; + if let Err(why) = dev.control(report).await? { + say(format_args!("SET_PROTOCOL(Report): {why:?}; not bound")); + return Ok(()); + } + } + let ask = Setup { request_type: STANDARD_FROM_INTERFACE, request: 6, value: u16::from(REPORT) << 8, index, length }; + let bytes = match dev.control(ask).await? { + Ok(bytes) => bytes, + Err(why) => { + say(format_args!("its report descriptor: {why:?}; not bound")); + return Ok(()); + } + }; + let parsed = match hid_descriptor::parse(&bytes) { + Ok(parsed) => parsed, + Err(why) => { + say(format_args!("its report descriptor is refused: {why:?}; not bound")); + return Ok(()); + } + }; + let mut hid = Hid::new(parsed); + let mut handles: Vec> = Vec::new(); + for (n, collection) in hid.collections().iter().enumerate() { + let handle = match collection.bound { + Ok(kind) => match usb.input.borrow_mut().bind() { + Some(handle) => { + say(format_args!("collection {n}: a {kind:?}")); + Some((handle, kind)) + } + None => { + say(format_args!("collection {n}: a {kind:?}, not bound: the owner's connection carries all it may")); + None + } + }, + Err(why) => { + say(format_args!("collection {n} ({:#06x}:{:#06x}): not bound, {why:?}", collection.page, collection.usage)); + None + } + }; + handles.push(handle); + } + if handles.iter().flatten().any(|(_, kind)| *kind == Kind::Keyboard) { + let idle = Setup { request_type: CLASS_TO_INTERFACE, request: SET_IDLE, value: 0, index, length: 0 }; + if let Err(why) = dev.control(idle).await? { + say(format_args!("SET_IDLE(0): {why:?}; it reports at its polling rate")); + } + } + let ended = read_reports(dev, ep.dci(), u32::from(ep.max_packet), &mut hid, &handles, &say).await; + let mut input = usb.input.borrow_mut(); + for (handle, _) in handles.iter().flatten() { + input.gone(*handle); + } + ended +} + +/// Every report the endpoint delivers, until the device leaves or the +/// endpoint will not run again. +async fn read_reports( + dev: &Device<'_, R, D, C, T>, + dci: u8, + max_packet: u32, + hid: &mut Hid, + handles: &[Option<(Handle, Kind)>], + say: &dyn Fn(core::fmt::Arguments<'_>), +) -> Result<(), Dead> { + let usb = dev.usb; + let Some(page) = dev.page() else { + say(format_args!("no page left in the grant for its reports; not read")); + return Ok(()); + }; + let at = dev.hc().dma.device_addr(page); + let length = max_packet.clamp(1, hc::PAGE as u32); + let mut refused = 0u32; + let mut counted = hid.counts; + loop { + if dev.leaving.get() { + return Ok(()); + } + let op = dev.submit(dci, &[Trb { param: at, status: length, control: hc::TRB_NORMAL | hc::TRB_ISP | hc::TRB_IOC }]); + match op.wait(dev.wake()).await { + Done::Gone => return Ok(()), + Done::Event { code: hc::CC_SUCCESS | hc::CC_SHORT_PACKET, moved, .. } => { + let report: Vec = (0..moved.min(length) as usize).map(|i| dev.read8(page + i)).collect(); + match hid.report(&report) { + Ok(Read::Keys { collection, .. }) => { + if let (Some((handle, _)), Some(held)) = (handles[collection], hid.held(collection)) { + usb.input.borrow_mut().keys(handle, held); + } + } + Ok(Read::Pointer { collection, report }) => { + if let Some((handle, _)) = handles[collection] { + usb.input.borrow_mut().pointer(handle, report); + } + } + Ok(Read::Unbound { .. }) => {} + Err(why) => { + refused += 1; + if refused <= SAID { + say(format_args!("a report refused: {why:?} ({report:02x?})")); + } + } + } + add(&usb.hid, &mut counted, hid.counts); + } + Done::Event { code, .. } => { + if dev.restart(dci, None).await?.is_err() { + say(format_args!("its endpoint ended with code {code} and would not run again; it is let go of")); + return Ok(()); + } + } + } + } +} + +/// Fold what one interface counted since `seen` into the driver's total. +fn add(total: &core::cell::Cell, seen: &mut Counts, now: Counts) { + let mut t = total.get(); + t.reserved += now.reserved - seen.reserved; + t.unlisted += now.unlisted - seen.unlisted; + t.unsent_button += now.unsent_button - seen.unsent_button; + t.unbound += now.unbound - seen.unbound; + total.set(t); + *seen = now; +} diff --git a/lib/xhci/src/identity.rs b/system/usbd/xhci/src/identity.rs similarity index 100% rename from lib/xhci/src/identity.rs rename to system/usbd/xhci/src/identity.rs diff --git a/system/usbd/xhci/src/input.rs b/system/usbd/xhci/src/input.rs new file mode 100644 index 00000000000..fd77a9ab602 --- /dev/null +++ b/system/usbd/xhci/src/input.rs @@ -0,0 +1,475 @@ +//! usbd's side of the `input` port: every bound keyboard and pointer, what +//! the owner of input was last told of each, and the connection that follows +//! them (`toyos_input::wire`). +//! +//! **The connection follows the devices.** It is opened when the first device +//! binds and dropped once the last one's `gone` is sent, so a controller that +//! carries only storage holds none of the owner's connections. While a device +//! is bound and there is none — the owner went, or never accepted — the next +//! report opens one, at most once a second ([`REOPEN_NS`]), and a report that +//! falls meanwhile is counted [`Counts::unowned`]. +//! +//! **usbd never blocks on the owner, and keeps no queue.** Per device it keeps +//! what the owner was last told and the device's current state; what is sent +//! is the difference, record by record, each `try_send`, and a full connection +//! stops the sending until it takes writes again. So a release is never lost, +//! and a press and release that both fall while it is full are never +//! delivered: they are counted [`Counts::coalesced`]. +//! +//! **A device's `source` is its number on this connection**, in bind order +//! from 0, never reused while the connection lasts: a late record never +//! reaches a later device. A fresh connection numbers the devices again. + +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use toyos_abi::input::{RawKeyEvent, MOD_RELEASED}; +use toyos_input::keys::{Keys, Usages}; +use toyos_input::wire::{Motion, Record, Report, DEVICES}; +use toyos_usbhid::report::Pointer; + +use crate::exec::Nanos; + +/// How often a device bound with no connection tries the owner again. +pub const REOPEN_NS: Nanos = 1_000_000_000; + +/// What the connection did with one record. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Sent { + Taken, + /// It holds all it can: try again once it takes writes. + Full, + /// The owner hung up. + Ended, +} + +/// How an open of the owner's port went. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Opened { + Opened, + /// None could be made now. + Refused, + /// This usbd was not given the port: it serves no input device. + NotGiven, +} + +/// The owner of input, as usbd reaches it. +pub trait Owner { + fn open(&mut self) -> Opened; + fn send(&mut self, record: &Record) -> Sent; + fn close(&mut self); +} + +/// What a device was told, or is now. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +struct State { + keys: Usages, + buttons: u8, + /// An absolute pointer's position. + at: Option<(u16, u16)>, + /// A relative pointer's motion and the wheel's, summed since told. + dx: i32, + dy: i32, + scroll: i32, +} + +struct Device { + source: u16, + told: State, + now: State, + /// Keys and buttons that moved since the owner was last told. + touched: Usages, + touched_buttons: u8, + /// Its `gone` is owed. + gone: bool, +} + +/// What the link counted, for `inspect`. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +pub struct Counts { + pub sent: u64, + pub coalesced: u64, + pub unowned: u64, + pub opened: u64, + pub refused: u64, +} + +/// Every bound device and the connection they share. +#[derive(Default)] +pub struct Inputs { + /// By the handle usbd's HID functions hold. + devices: BTreeMap, + next_handle: u32, + next_source: u32, + connected: bool, + /// Every number a connection may give out was given: the next flush + /// drops it, and the one after numbers the devices afresh. + spent: bool, + full: bool, + /// A report arrived with no connection since the last open was tried. + asked: bool, + last_open: Option, + /// The port is not this usbd's: nothing is bound. + not_given: bool, + pub counts: Counts, +} + +/// A bound device, as its HID function names it. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Handle(u32); + +impl Inputs { + /// A device bound: `None` once [`DEVICES`] are, which is what one + /// connection may carry. + pub fn bind(&mut self) -> Option { + if self.not_given || self.devices.values().filter(|d| !d.gone).count() >= DEVICES { + return None; + } + let handle = self.next_handle; + self.next_handle += 1; + let source = self.source(); + self.devices.insert(handle, Device { source, told: State::default(), now: State::default(), touched: Usages::NONE, touched_buttons: 0, gone: false }); + self.asked = true; + Some(Handle(handle)) + } + + fn source(&mut self) -> u16 { + let source = self.next_source; + self.next_source += 1; + self.spent |= source > u32::from(u16::MAX); + source as u16 + } + + + fn reported(&mut self) { + if !self.connected { + self.counts.unowned += 1; + self.asked = true; + } + } + + /// A keyboard now holds `keys`. + pub fn keys(&mut self, handle: Handle, keys: Usages) { + // A device unbound with the port that was found not to be this + // usbd's says nothing. + let Some(device) = self.devices.get_mut(&handle.0) else { return }; + device.touched = device.touched.union(Usages::symmetric(device.now.keys, keys)); + device.now.keys = keys; + self.reported(); + } + + /// A pointer reported `report`. + pub fn pointer(&mut self, handle: Handle, report: Pointer) { + let Some(device) = self.devices.get_mut(&handle.0) else { return }; + device.touched_buttons |= device.now.buttons ^ report.buttons; + device.now.buttons = report.buttons; + device.now.scroll = device.now.scroll.saturating_add(i32::from(report.scroll)); + match report.motion { + Motion::Absolute { x, y } => device.now.at = Some((x, y)), + Motion::Relative { dx, dy } => { + device.now.dx = device.now.dx.saturating_add(i32::from(dx)); + device.now.dy = device.now.dy.saturating_add(i32::from(dy)); + } + } + self.reported(); + } + + /// The device left: what it holds is released, and its `gone` is owed. + pub fn gone(&mut self, handle: Handle) { + if let Some(device) = self.devices.get_mut(&handle.0) { + device.gone = true; + } + } + + /// Whether the connection is full, and the caller should send again once + /// it takes writes. + pub fn full(&self) -> bool { + self.connected && self.full + } + + /// Tell the owner every difference it has not been told, opening or + /// dropping the connection as the devices ask. + pub fn flush(&mut self, owner: &mut impl Owner, now: Nanos) { + if self.connected && self.spent { + owner.close(); + self.connected = false; + self.asked = true; + self.last_open = None; + } + if !self.connected { + if self.devices.values().all(|d| d.gone) { + // Nobody bound: what the last connection owed it ended with it. + self.devices.clear(); + return; + } + let due = self.last_open.is_none_or(|at| now >= at.saturating_add(REOPEN_NS)); + if !self.asked || !due { + return; + } + self.asked = false; + self.last_open = Some(now); + match owner.open() { + Opened::Opened => {} + Opened::Refused => { + self.counts.refused += 1; + return; + } + Opened::NotGiven => { + self.not_given = true; + self.devices.clear(); + return; + } + } + self.counts.opened += 1; + self.connected = true; + self.full = false; + // A fresh owner knows nothing: every device is numbered again and + // told its whole state. + self.devices.retain(|_, d| !d.gone); + self.next_source = 0; + self.spent = false; + let handles: Vec = self.devices.keys().copied().collect(); + for handle in handles { + let source = self.source(); + let d = self.devices.get_mut(&handle).expect("listed"); + d.source = source; + d.told = State::default(); + d.touched = Usages::NONE; + d.touched_buttons = 0; + } + } + self.full = false; + let handles: Vec = self.devices.keys().copied().collect(); + for handle in handles { + match self.tell(owner, handle) { + Sent::Taken => {} + Sent::Full => { + self.full = true; + return; + } + Sent::Ended => { + self.ended(owner); + return; + } + } + } + if self.devices.is_empty() { + self.connected = false; + owner.close(); + } + } + + /// The owner hung up: what it held of every device is released by its + /// own end of the connection, and the next report opens another. + pub fn ended(&mut self, owner: &mut impl Owner) { + if self.connected { + self.connected = false; + owner.close(); + } + } + + /// One device's difference, record by record; each one taken moves what + /// the owner was told. + fn tell(&mut self, owner: &mut impl Owner, handle: u32) -> Sent { + let device = self.devices.get_mut(&handle).expect("listed"); + let source = device.source; + if device.gone { + let sent = owner.send(&Record::Gone { source }); + if sent == Sent::Taken { + self.counts.sent += 1; + self.devices.remove(&handle); + } + return sent; + } + // A key that moved and moved back since it was told is a press and + // its release the owner will never see. + let unseen = device.touched.without(Usages::symmetric(device.told.keys, device.now.keys)); + let mut keys = Keys::default(); + let _ = keys.hold(device.told.keys); + for t in keys.hold(device.now.keys) { + let modifiers = if t.pressed { 0 } else { MOD_RELEASED }; + let sent = owner.send(&Record::Key { source, key: RawKeyEvent { keycode: t.usage, modifiers } }); + if sent != Sent::Taken { + return sent; + } + self.counts.sent += 1; + if t.pressed { + device.told.keys.insert(t.usage); + } else { + device.told.keys.remove(t.usage); + } + } + self.counts.coalesced += u64::from(unseen.count()); + device.touched = Usages::NONE; + + let moved = device.now.dx != 0 || device.now.dy != 0 || device.now.scroll != 0; + let changed = device.told.buttons != device.now.buttons || device.told.at != device.now.at; + if moved || changed { + let clamp16 = |v: i32| v.clamp(i32::from(i16::MIN), i32::from(i16::MAX)); + let (dx, dy) = (clamp16(device.now.dx), clamp16(device.now.dy)); + let scroll = device.now.scroll.clamp(i32::from(i8::MIN), i32::from(i8::MAX)); + let motion = match device.now.at { + Some((x, y)) => Motion::Absolute { x, y }, + None => Motion::Relative { dx: dx as i16, dy: dy as i16 }, + }; + let report = Report { source, buttons: device.now.buttons, scroll: scroll as i8, motion }; + let sent = owner.send(&Record::Pointer(report)); + if sent != Sent::Taken { + return sent; + } + self.counts.sent += 1; + let unseen = device.touched_buttons & !(device.told.buttons ^ device.now.buttons); + self.counts.coalesced += u64::from(unseen.count_ones()); + device.touched_buttons = 0; + device.told.buttons = device.now.buttons; + device.told.at = device.now.at; + if device.now.at.is_none() { + device.now.dx -= dx; + device.now.dy -= dy; + } + device.now.scroll -= scroll; + } + Sent::Taken + } +} + +#[cfg(test)] +mod tests { + use super::*; + use toyos_input::wire::Record; + + const SECOND: Nanos = REOPEN_NS; + + #[derive(Default)] + struct Mock { + connected: bool, + room: Option, + refuses: bool, + opens: u32, + connections: Vec>, + } + + impl Owner for Mock { + fn open(&mut self) -> Opened { + self.opens += 1; + if self.refuses { + return Opened::Refused; + } + self.connected = true; + self.connections.push(Vec::new()); + Opened::Opened + } + fn send(&mut self, record: &Record) -> Sent { + if !self.connected { + return Sent::Ended; + } + match &mut self.room { + Some(0) => return Sent::Full, + Some(room) => *room -= 1, + None => {} + } + self.connections.last_mut().unwrap().push(*record); + Sent::Taken + } + fn close(&mut self) { + self.connected = false; + } + } + + fn held(usages: &[u8]) -> Usages { + let mut u = Usages::NONE; + for &usage in usages { + u.insert(usage); + } + u + } + + fn key(source: u16, keycode: u8, pressed: bool) -> Record { + Record::Key { source, key: RawKeyEvent { keycode, modifiers: if pressed { 0 } else { MOD_RELEASED } } } + } + + /// At a full connection nothing waits in usbd: a release that falls while + /// it is full reaches the owner once it drains, and a press and release + /// inside it are counted, never delivered. + #[test] + fn at_a_full_input_connection_no_record_waits_and_a_release_reaches_the_owner_once_it_drains() { + let mut inputs = Inputs::default(); + let mut owner = Mock::default(); + let kb = inputs.bind().unwrap(); + inputs.keys(kb, held(&[0x04])); + inputs.flush(&mut owner, 0); + assert_eq!(owner.connections, [vec![key(0, 0x04, true)]]); + owner.room = Some(0); + inputs.keys(kb, held(&[])); + inputs.keys(kb, held(&[0x05])); + inputs.keys(kb, held(&[])); + inputs.flush(&mut owner, 1); + assert!(inputs.full()); + owner.room = None; + inputs.flush(&mut owner, 2); + assert!(!inputs.full()); + assert_eq!(owner.connections, [vec![key(0, 0x04, true), key(0, 0x04, false)]]); + assert_eq!(inputs.counts.coalesced, 1, "the press and release of 0x05 inside the full span"); + } + + /// A controller with only a stick opens nothing; the first bind opens the + /// connection; the last device's `gone` is sent and the connection dropped. + #[test] + fn the_connection_follows_the_input_devices() { + let mut inputs = Inputs::default(); + let mut owner = Mock::default(); + inputs.flush(&mut owner, 0); + assert_eq!(owner.opens, 0, "a controller with nothing bound opened a connection"); + let kb = inputs.bind().unwrap(); + let mouse = inputs.bind().unwrap(); + inputs.flush(&mut owner, 1); + assert_eq!((owner.opens, owner.connected), (1, true)); + inputs.gone(kb); + inputs.flush(&mut owner, 2); + assert!(owner.connected, "dropped with a device still bound"); + inputs.gone(mouse); + inputs.flush(&mut owner, 3); + assert_eq!(owner.connections, [vec![Record::Gone { source: 0 }, Record::Gone { source: 1 }]]); + assert!(!owner.connected, "kept after the last gone"); + } + + /// The owner drops the connection while a key is held: the next report + /// opens another, which is told the held key. A second drop, and a + /// report inside the second after the last open, opens nothing and is + /// counted; the first report a second after it opens again. + #[test] + fn a_bound_device_reopens_its_connection_on_its_next_report() { + let mut inputs = Inputs::default(); + let mut owner = Mock::default(); + let kb = inputs.bind().unwrap(); + inputs.keys(kb, held(&[0x04])); + inputs.flush(&mut owner, 0); + inputs.ended(&mut owner); + inputs.flush(&mut owner, 2 * SECOND); + assert_eq!(owner.opens, 1, "reopened with no report"); + inputs.keys(kb, held(&[0x04, 0x05])); + inputs.flush(&mut owner, 2 * SECOND); + assert_eq!(owner.opens, 2); + assert_eq!(owner.connections[1], [key(0, 0x04, true), key(0, 0x05, true)]); + inputs.ended(&mut owner); + let unowned = inputs.counts.unowned; + inputs.keys(kb, held(&[0x04])); + inputs.flush(&mut owner, 2 * SECOND + SECOND / 2); + assert_eq!(owner.opens, 2, "a report inside the second opened"); + assert_eq!(inputs.counts.unowned, unowned + 1); + inputs.keys(kb, held(&[])); + inputs.flush(&mut owner, 3 * SECOND); + assert_eq!(owner.opens, 3); + assert_eq!(owner.connections[2], [], "a fresh owner is told the state now: nothing held"); + } + + /// One connection carries at most `DEVICES` devices. + #[test] + fn a_device_past_what_a_connection_carries_is_not_bound() { + let mut inputs = Inputs::default(); + for _ in 0..DEVICES { + assert!(inputs.bind().is_some()); + } + assert!(inputs.bind().is_none()); + } +} diff --git a/lib/xhci/src/invariants.rs b/system/usbd/xhci/src/invariants.rs similarity index 92% rename from lib/xhci/src/invariants.rs rename to system/usbd/xhci/src/invariants.rs index d51b1a5d796..7d05206aa27 100644 --- a/lib/xhci/src/invariants.rs +++ b/system/usbd/xhci/src/invariants.rs @@ -1,8 +1,8 @@ //! What must be true of a port after every step, whatever sequence produced it. //! -//! Separate from the tests because the simulator checks these after *every* -//! step of every generated sequence, where a test checks an outcome after a -//! sequence it chose. [`Violation`] is the whole list. +//! Separate from the tests because a test build checks these after *every* +//! step a port's task takes and every PORTSC write the stub receives, where a +//! test checks an outcome after a sequence it chose. [`Violation`] is the whole list. use crate::port::{PortState, Step}; use crate::portsc::{self, Portsc}; diff --git a/system/usbd/xhci/src/ladder.rs b/system/usbd/xhci/src/ladder.rs new file mode 100644 index 00000000000..12674dc99cd --- /dev/null +++ b/system/usbd/xhci/src/ladder.rs @@ -0,0 +1,242 @@ +//! What a mass-storage device's transport is owed after a break: a ladder of +//! three rungs, each ended by the device's own answer and by nothing else. +//! +//! 1. [`Rung::ClassReset`] — Bulk-Only Transport 1.0 §5.3.4's Reset Recovery +//! (`msc`'s class reset), which the class defines and a device may +//! answer without honouring: §3.1 has it "ready … for the next CBW" once it +//! answers the reset, and the requests give a host no way to tell a device +//! that is from one that is still inside the command it was given. +//! 2. [`Rung::PortReset`] — the reset no USB device may ignore (USB 2.0 §9.1.1, +//! Figure 9-1: every state returns to Default on it), and the enumeration +//! that follows it, the disk held for the device's return under the number +//! it already has, so the volume on it carries on (`identity`). +//! 3. [`Rung::Offline`] — the device did not answer after a port reset. Its +//! endpoints are stopped and **the last thing it is sent is a reset**, so +//! whatever command it was inside is over and the next host finds a device +//! in its Default state. +//! +//! **Verified means a TEST UNIT READY answered under its own tag.** It is the +//! one command with no data phase, so it asks the device to move nothing. +//! +//! **A device owed Data-Out bytes is not given the class reset** ([`enters_at`]). +//! The rung's verification is itself 31 bytes on the Bulk-Out, and a device +//! that answered the class reset without leaving its data phase takes them as +//! the data it is owed: the question cannot be asked there without being part +//! of the answer. The port reset's can — after a reset and a +//! SET_CONFIGURATION no data phase is open (USB 2.0 §9.1.1, §9.4.7) — so that +//! is where such a break enters, and nothing reaches the Bulk-Out before it. +//! +//! **A rung is climbed once per run of breaks.** A rung that verified and whose +//! command then broke again has not brought the transport back, and asking it +//! again asks the same question of the same device: the next break climbs on. +//! Only a completed round trip ends the run. + +/// One rung of the ladder. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum Rung { + ClassReset, + PortReset, + Offline, +} + +impl Rung { + /// What a line about this rung calls it. + pub fn named(self) -> &'static str { + match self { + Self::ClassReset => "class reset", + Self::PortReset => "port reset", + Self::Offline => "last reset", + } + } +} + +/// The most breaks a run can hold before its device is offline: one per rung. +pub const MOST_BREAKS: u8 = 3; + +/// Where in its round trip a break left the device. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Left { + /// Inside a Data-Out phase: it holds a command block and is owed bytes the + /// host has stopped sending. + OwedDataOut, + /// Anywhere else: no command block taken, a Data-In it is sending, or a + /// status it is holding. + Elsewhere, +} + +/// Where a break in `phase` of a command left the device; `data_out` is whether +/// the command has a Data-Out phase. +/// +/// **A command-phase break of such a command is a device owed data too**: a CBW +/// whose handshake was lost after the device took it is a command the device +/// holds, and the bytes it then waits for are the Data-Out phase's. Only a break +/// in the status phase is past the data. +pub fn left(phase: crate::bot::Phase, data_out: bool) -> Left { + use crate::bot::Phase; + match phase { + Phase::Command | Phase::DataOwed | Phase::Data if data_out => Left::OwedDataOut, + _ => Left::Elsewhere, + } +} + +/// The lowest rung a break that left the device there may be answered with. +pub fn enters_at(left: Left) -> Rung { + match left { + Left::OwedDataOut => Rung::PortReset, + Left::Elsewhere => Rung::ClassReset, + } +} + +/// The rung a break climbs to: above every rung this run of breaks has climbed +/// (`climbed` is the highest), and never below where the break itself enters. +/// A rung that did not verify is the next break, [`Left::Elsewhere`]. +pub fn next(climbed: Option, left: Left) -> Rung { + let above = match climbed { + None => Rung::ClassReset, + Some(Rung::ClassReset) => Rung::PortReset, + Some(Rung::PortReset | Rung::Offline) => Rung::Offline, + }; + above.max(enters_at(left)) +} + +/// A device's run of breaks: how many, and the highest rung among them. +/// +/// **Per device, not per command**: the run it bounds is the device's, however +/// many callers and operations it is spread over, and only a completed round +/// trip ends it ([`Self::over`]). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Run { + breaks: u8, + climbed: Option, +} + +/// What one break costs the run. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Climb { + pub rung: Rung, + /// The run's first rung is above the class reset: the device is owed the + /// Data-Out of the command that broke ([`enters_at`]). + pub skips_class_reset: bool, +} + +impl Run { + pub const NONE: Self = Self { breaks: 0, climbed: None }; + + /// Breaks in the run so far. + pub fn breaks(self) -> u8 { + self.breaks + } + + /// A break that left the device `left`: counted, and the rung it climbs. + pub fn broke(&mut self, left: Left) -> Climb { + self.breaks = self.breaks.saturating_add(1); + let rung = next(self.climbed, left); + let skips_class_reset = self.climbed.is_none() && rung != Rung::ClassReset; + self.climbed = Some(rung); + Climb { rung, skips_class_reset } + } + + /// A round trip completed: the run is over. How many breaks it held. + pub fn over(&mut self) -> u8 { + core::mem::replace(self, Self::NONE).breaks + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const LEFT: [Left; 2] = [Left::OwedDataOut, Left::Elsewhere]; + + /// Every run, however its breaks left the device: no rung is climbed + /// twice, none below where a break enters, the run is offline within + /// [`MOST_BREAKS`], and offline is never reached before a port reset. + #[test] + fn every_run_of_breaks_climbs_strictly_and_ends_offline_after_a_port_reset() { + for first in LEFT { + for second in LEFT { + for third in LEFT { + let mut climbed = None; + let mut rungs = [None; MOST_BREAKS as usize]; + for (at, left) in [first, second, third].into_iter().enumerate() { + let rung = next(climbed, left); + assert!(climbed.is_none_or(|last| rung > last), "{climbed:?} then {rung:?}"); + assert!(rung >= enters_at(left)); + rungs[at] = Some(rung); + climbed = Some(rung); + if rung == Rung::Offline { + break; + } + } + assert_eq!(climbed, Some(Rung::Offline), "{first:?} {second:?} {third:?}"); + let offline = rungs.iter().position(|r| *r == Some(Rung::Offline)).expect("reached"); + assert_eq!(rungs[offline - 1], Some(Rung::PortReset), "{rungs:?}"); + } + } + } + } + + /// The run is the device's: every break climbs above the last, the count + /// reaches [`MOST_BREAKS`] exactly where the run is offline, and a completed + /// round trip starts the next run at the bottom. + #[test] + fn a_run_climbs_one_rung_per_break_until_a_round_trip_ends_it() { + let mut run = Run::NONE; + assert_eq!(run.broke(Left::Elsewhere), Climb { rung: Rung::ClassReset, skips_class_reset: false }); + assert_eq!(run.broke(Left::Elsewhere), Climb { rung: Rung::PortReset, skips_class_reset: false }); + assert_eq!(run.broke(Left::Elsewhere).rung, Rung::Offline); + assert_eq!(run.breaks(), MOST_BREAKS); + assert_eq!(run.over(), MOST_BREAKS); + assert_eq!(run, Run::NONE); + assert_eq!(run.broke(Left::Elsewhere).rung, Rung::ClassReset, "a run begun again climbs from the bottom"); + } + + /// Said once, at the first rung of a run, and only when that rung is the + /// port reset. + #[test] + fn only_a_run_that_begins_owed_data_out_skips_the_class_reset() { + let mut run = Run::NONE; + assert_eq!(run.broke(Left::OwedDataOut), Climb { rung: Rung::PortReset, skips_class_reset: true }); + assert_eq!(run.broke(Left::OwedDataOut), Climb { rung: Rung::Offline, skips_class_reset: false }); + let mut run = Run::NONE; + run.broke(Left::Elsewhere); + assert!(!run.broke(Left::OwedDataOut).skips_class_reset, "the class reset was climbed"); + } + + /// Every run that reaches a rung below [`Rung::Offline`], each way that + /// rung can end unverified: its device left exactly where its port no + /// longer holds it, which is no break; otherwise the rung is the next + /// break, and the next rung quiesces against the event that ended the + /// TEST UNIT READY, where one did. + + #[test] + fn a_break_elsewhere_climbs_every_rung() { + assert_eq!(next(None, Left::Elsewhere), Rung::ClassReset); + assert_eq!(next(Some(Rung::ClassReset), Left::Elsewhere), Rung::PortReset); + assert_eq!(next(Some(Rung::PortReset), Left::Elsewhere), Rung::Offline); + } + + #[test] + fn a_device_owed_data_out_is_never_given_the_class_reset() { + for climbed in [None, Some(Rung::ClassReset), Some(Rung::PortReset)] { + assert_ne!(next(climbed, Left::OwedDataOut), Rung::ClassReset, "{climbed:?}"); + } + assert_eq!(next(None, Left::OwedDataOut), Rung::PortReset); + } + + /// A command that sends data leaves its device owed it from the moment its + /// CBW may have been taken until its data phase is done; nothing else does. + #[test] + fn a_break_before_a_data_out_phase_is_done_leaves_the_device_owed_it() { + use crate::bot::Phase; + for phase in [Phase::Command, Phase::DataOwed, Phase::Data] { + assert_eq!(left(phase, true), Left::OwedDataOut, "{phase:?}"); + assert_eq!(left(phase, false), Left::Elsewhere, "{phase:?} of a command sending nothing"); + } + for phase in [Phase::Closed, Phase::StatusOwed, Phase::Status] { + for data_out in [true, false] { + assert_eq!(left(phase, data_out), Left::Elsewhere, "{phase:?}"); + } + } + } +} diff --git a/system/usbd/xhci/src/lib.rs b/system/usbd/xhci/src/lib.rs new file mode 100644 index 00000000000..0fd918f3872 --- /dev/null +++ b/system/usbd/xhci/src/lib.rs @@ -0,0 +1,55 @@ +//! usbd's driver: one xHCI controller and every device on its root hub, every +//! decision it makes, and none of the instructions that carry them out. +//! +//! # The boundary +//! +//! `toyos-device-memory`'s `Registers`, one access to the mapped BAR, and +//! `DmaBuffers`, the one grant the controller reaches through its IOMMU +//! domain, and [`exec::Host`], the clock and where lines go: each a generic +//! parameter. usbd's binary is that boundary one instruction deep; on the host +//! it is the `stub` xHC, written from xHCI 1.2. +//! +//! # The shape +//! +//! [`hc`] is the register file, the rings and the operations the controller +//! holds; [`usb`] the root hub, one task per port, enumeration, and each +//! device's functions; [`hid`] and [`msc`] the two classes bound; [`input`] +//! the link to the owner of input. Everything runs as tasks on one thread +//! ([`exec`]), each waiting on one thing: the answer to the TRB it submitted, +//! a deadline, or a change another task made. +//! +//! The rest are decoders of the controller's and the devices' words, and +//! decisions with no transfer in them: [`descriptor`], [`xecp`], +//! [`protocol`], [`portsc`], [`port`], [`configure`], [`recovery`], [`bot`], +//! [`ladder`], [`identity`], [`flush`]. + +#![forbid(unsafe_code)] + +extern crate alloc; + +pub mod bot; +pub mod configure; +pub mod descriptor; +pub mod exec; +pub mod flush; +pub mod hc; +pub mod hid; +pub mod identity; +pub mod input; +pub mod invariants; +pub mod ladder; +pub mod msc; +pub mod port; +pub mod portsc; +pub mod protocol; +pub mod recovery; +pub mod usb; +pub mod xecp; + +#[cfg(test)] +mod stub; +#[cfg(test)] +mod tests; + +pub use portsc::{LinkState, Portsc}; +pub use protocol::{Protocol, Protocols}; diff --git a/system/usbd/xhci/src/msc.rs b/system/usbd/xhci/src/msc.rs new file mode 100644 index 00000000000..4c51710cd2f --- /dev/null +++ b/system/usbd/xhci/src/msc.rs @@ -0,0 +1,812 @@ +//! A mass-storage interface, SCSI over Bulk-Only Transport 1.0, served as a +//! disk whose work the block service's sessions queue ([`Disks`]). +//! +//! **One command at a time per device**, as Bulk-Only has it (§5.1): a CBW, +//! a data phase moved straight between the device and the client's region, +//! and a CSW held to §6.3 ([`crate::bot`]). Where a command breaks, the +//! ladder answers ([`crate::ladder`]): Reset Recovery (§5.3.4) verified by a +//! TEST UNIT READY under its own tag; then a port reset and enumeration +//! again, the disk held for its device's return ([`crate::identity`]); then +//! offline, the last thing it is sent a reset. +//! +//! **A command's bound is policy.** BOT and SBC give a command no time +//! limit, and a stick has answered later than two seconds +//! (`issues/a-stick-that-answers-late-is-broken-by-the-two-second-abandon.md`): +//! [`COMMAND_NS`]. + +use alloc::collections::{BTreeMap, VecDeque}; +use alloc::format; +use alloc::string::String; +use alloc::vec::Vec; +use core::cell::{Cell, RefCell}; +use core::num::{NonZeroU16, NonZeroU64}; + +use toyos_device_memory::{DmaBuffers, Registers}; +use toyos_scsi::{self as scsi, Cdb, Geometry, Reply, Sense, HOST_BLOCK, SENSE_BYTES}; + +use crate::bot::{self, Answer, Bot, Broke, Pipe, RoundTrip, CBW_LEN, CSW_LEN}; +use crate::configure::Configure; +use crate::descriptor::{Endpoint, Interface, BULK}; +use crate::exec::{self, Dead, Host, Nanos}; +use crate::flush::Debt; +use crate::hc::{self, Done, Trb, TRB_MOST}; +use crate::identity::{self, Identity, Serial, UsbId}; +use crate::ladder::{Left, Rung, Run}; +use crate::usb::{Ask, Device, Setup, Usb}; + +/// How long one Bulk-Only transfer may go unanswered before it is taken +/// back. Policy, on the reasoning behind diskserver's own bound: the slowest +/// thing a healthy device does is flush a large cache. +pub const COMMAND_NS: Nanos = 10_000_000_000; +/// How long a device that answers NOT READY is asked again before it is +/// called unready. Policy: a stick spins up in well under it. +const READY_NS: Nanos = 10_000_000_000; +/// Work one disk holds queued at once. +pub const QUEUE: usize = 32; +/// Most READ(10)s of a bring-up's table read before it is refused. +const TABLE_READS: usize = 64; + +/// The silence a transfer that went unanswered ended in. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Silent; + +/// One piece of work a session queued on a disk; `token` is the session's +/// own name for it, answered back. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Work { + /// `blocks` host blocks at host block `block`, to or from device address + /// `at`, which the controller's domain maps. + Io { write: bool, block: u64, blocks: u32, at: u64, token: (u64, u32) }, + /// The device's volatile cache to the medium. + Flush { token: (u64, u32) }, +} + +impl Work { + fn token(&self) -> (u64, u32) { + match *self { + Self::Io { token, .. } | Self::Flush { token } => token, + } + } +} + +/// What happened to a disk, for its sessions. +#[derive(Debug)] +pub enum Event { + /// A disk came up, and this is its table. + Arrived { disk: u32, name: String, table: T }, + /// One piece of work was answered; `status` is the sense the device gave + /// a refusal (key, ASC and ASCQ), or 0. + Answered { disk: u32, token: (u64, u32), write: bool, ok: bool, status: u32 }, + /// The device came back from a reset that may have lost its cache: + /// every piece of work queued is dropped unanswered, and the sessions + /// answer it. + Reset { disk: u32 }, + /// The disk left: everything queued was answered not done. + Gone { disk: u32 }, +} + +struct Disk { + identity: Identity, + geometry: Geometry, + work: VecDeque, + /// The device is away, held for its return until then. + away: Option, + /// When this driver last reset its port. + reset_at: Option, + no_cache: bool, + debt: Debt, +} + +/// Every disk on the controller. +pub struct Disks { + disks: RefCell>, + events: RefCell>>, + next: Cell, + /// Mass-storage functions bound and not yet serving or refused. + binding: Cell, + submitted: Cell, +} + +impl Default for Disks { + fn default() -> Self { + Self { disks: RefCell::default(), events: RefCell::default(), next: Cell::new(0), binding: Cell::new(0), submitted: Cell::new(0) } + } +} + +/// A mass-storage function counted as not yet settled, until it drops. +pub struct Binding<'d>(&'d Cell); + +impl Drop for Binding<'_> { + fn drop(&mut self) { + self.0.set(self.0.get() - 1); + } +} + +impl Disks { + pub fn binding(&self) -> Binding<'_> { + self.binding.set(self.binding.get() + 1); + Binding(&self.binding) + } + + pub(crate) fn settled(&self) -> bool { + self.binding.get() == 0 + } + + /// Everything that happened since the last call. + pub fn take_events(&self) -> Vec> { + self.events.borrow_mut().drain(..).collect() + } + + /// Whether disk `disk` takes one more piece of work now. + pub fn has_room(&self, disk: u32) -> bool { + self.disks.borrow().get(&disk).is_some_and(|d| d.work.len() < QUEUE) + } + + /// Whether a flush of disk `disk` is issued at all: a device that said it + /// has no cache has its writes on the medium once answered. + pub fn cached(&self, disk: u32) -> bool { + self.disks.borrow().get(&disk).is_some_and(|d| !d.no_cache) + } + + /// Queue `work` on disk `disk`, which its sessions were offered and which + /// has not gone. + pub fn submit(&self, disk: u32, work: Work) { + let mut disks = self.disks.borrow_mut(); + let d = disks.get_mut(&disk).unwrap_or_else(|| panic!("usbd: work queued on disk {disk}, which is not served")); + assert!(d.work.len() < QUEUE, "usbd: work queued on disk {disk} past its room"); + d.work.push_back(work); + self.submitted.set(self.submitted.get() + 1); + } + + /// How many pieces of work were ever queued: a caller that queued some + /// runs the driver before it waits. + pub fn submitted(&self) -> u64 { + self.submitted.get() + } + + fn event(&self, event: Event) { + self.events.borrow_mut().push_back(event); + } + + /// The disk is gone: what it holds is answered not done. + fn end(&self, disk: u32) { + let Some(d) = self.disks.borrow_mut().remove(&disk) else { return }; + for work in d.work { + let write = matches!(work, Work::Io { write: true, .. }); + self.event(Event::Answered { disk, token: work.token(), write, ok: false, status: 0 }); + } + self.event(Event::Gone { disk }); + } + + fn next_expiry(&self) -> Option { + self.disks.borrow().values().filter_map(|d| d.away).min() + } + + /// Ends every disk held past its device's return window. + pub(crate) async fn keep(&self, usb: &Usb) -> Result<(), Dead> { + loop { + let Some(at) = self.next_expiry() else { + exec::until(&usb.wake, || self.next_expiry().map(|_| ())).await; + continue; + }; + let changed = exec::until_or(&usb.wake, at, || (self.next_expiry() != Some(at)).then_some(())).await; + if changed.is_some() { + continue; + } + let expired: Vec = self + .disks + .borrow() + .iter() + .filter(|(_, d)| d.away.is_some_and(|away| away <= at)) + .map(|(n, _)| *n) + .collect(); + for disk in expired { + usb.say(format_args!("disk {disk}: its device did not come back; it is gone")); + self.end(disk); + } + } + } +} + +/// The disk a mass-storage interface serves, and its pipes. +struct Msc<'d, 'u, R: Registers, D: DmaBuffers, C: Host, T> { + dev: &'d Device<'u, R, D, C, T>, + interface: u8, + bulk_in: Endpoint, + bulk_out: Endpoint, + in_packet: NonZeroU16, + out_packet: NonZeroU16, + /// CBW at 0, CSW at 64, a small data phase at 512. + io: usize, + /// A bring-up read's sectors. + block: usize, + tag: u32, + run: Run, + /// The ladder asked the port for something: this function ends. + asked: bool, +} + +const AT_CSW: usize = 64; +const AT_SMALL: usize = 512; + +/// A data phase: where, which way, how long. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Data { + None, + In { at: u64, len: u32 }, + Out { at: u64, len: u32 }, +} + +impl Data { + fn len(self) -> u32 { + match self { + Self::None => 0, + Self::In { len, .. } | Self::Out { len, .. } => len, + } + } +} + +pub async fn serve( + dev: &Device<'_, R, D, C, T>, + interface: Interface, + binding: Binding<'_>, +) -> Result<(), Dead> { + let usb = dev.usb; + let name = format!("usb port {} interface {}", dev.port + 1, interface.number); + let say = |line: core::fmt::Arguments<'_>| usb.say(format_args!("{name}: {line}")); + let pipe = |inbound: bool| interface.endpoints.iter().copied().find(|e| e.kind == BULK && e.is_in() == inbound); + let (Some(bulk_in), Some(bulk_out)) = (pipe(true), pipe(false)) else { + say(format_args!("Bulk-Only with no bulk pair; not bound")); + return Ok(()); + }; + let (Some(in_packet), Some(out_packet)) = (NonZeroU16::new(bulk_in.max_packet), NonZeroU16::new(bulk_out.max_packet)) else { + say(format_args!("a bulk endpoint states a max packet size of 0; not bound")); + return Ok(()); + }; + let (Some(io), Some(block)) = (dev.page(), dev.page()) else { + say(format_args!("no pages left in the grant; not bound")); + return Ok(()); + }; + let run = usb.runs.borrow_mut().remove(&dev.port).unwrap_or(Run::NONE); + let mut msc = Msc { dev, interface: interface.number, bulk_in, bulk_out, in_packet, out_packet, io, block, tag: 0, run, asked: false }; + let serial = msc.serial().await?; + let Some((geometry, inquiry)) = msc.bring_up(&say).await? else { return Ok(()) }; + let device = dev.descriptor.get().expect("enumerated"); + let identity = Identity { + usb: UsbId { vendor: device.vendor, product: device.product, release: device.release }, + serial, + inquiry: inquiry.0, + sectors: geometry.sectors(), + sector_bytes: geometry.sector_bytes(), + }; + say(format_args!( + "{} {} ({} sectors of {} bytes)", + scsi::Printable(inquiry.vendor()), + scsi::Printable(inquiry.product()), + geometry.sectors(), + geometry.sector_bytes() + )); + let disks = &usb.disks; + let disk = match adopt(disks, &identity, usb) { + Some(disk) => disk, + None => { + let table = msc.table(&geometry).await?; + if msc.asked { + return Ok(()); + } + let disk = disks.next.get(); + disks.next.set(disk + 1); + let held = Disk { identity, geometry, work: VecDeque::new(), away: None, reset_at: None, no_cache: false, debt: Debt::NONE }; + disks.disks.borrow_mut().insert(disk, held); + disks.event(Event::Arrived { disk, name: format!("usb disk {disk}"), table }); + say(format_args!("serving it as disk {disk}")); + disk + } + }; + drop(binding); + msc.work(disk).await?; + // The device left or was let go of: its disk is held for a device this + // driver reset, and gone otherwise. + let now = usb.wake.now(); + let mut held = disks.disks.borrow_mut(); + let d = held.get_mut(&disk).expect("served until here"); + match identity::returns_by(d.reset_at, now) { + Some(by) if !msc.offline() => { + d.away = Some(by); + d.debt = Debt::adopted(d.debt.left(d.no_cache)); + drop(held); + say(format_args!("disk {disk} is held for its device's return")); + } + _ => { + drop(held); + say(format_args!("disk {disk} is gone")); + disks.end(disk); + } + } + Ok(()) +} + +/// The held disk `identity` is the device of, taken back; its sessions are +/// told of a reset if it may have lost writes it reported. +fn adopt(disks: &Disks, identity: &Identity, usb: &Usb) -> Option { + let mut all = disks.disks.borrow_mut(); + let (&disk, d) = all.iter_mut().find(|(_, d)| d.away.is_some() && identity::same(&d.identity, identity).is_ok())?; + d.away = None; + let lost = d.debt.losses() > 0; + if lost { + d.work.clear(); + } + drop(all); + usb.say(format_args!("disk {disk}'s device came back")); + if lost { + disks.event(Event::Reset { disk }); + } + Some(disk) +} + +impl<'d, 'u, R: Registers, D: DmaBuffers, C: Host, T> Msc<'d, 'u, R, D, C, T> { + fn offline(&self) -> bool { + self.dev.ask.get() == Some(Ask::Offline) + } + + fn addr(&self, at: usize) -> u64 { + self.dev.hc().dma.device_addr(at) + } + + fn dci(&self, pipe: Pipe) -> u8 { + match pipe { + Pipe::In => self.bulk_in.dci(), + Pipe::Out => self.bulk_out.dci(), + } + } + + /// The string its iSerialNumber names (USB 2.0 §9.6.7), in the first + /// language string descriptor zero offers. + async fn serial(&mut self) -> Result { + let index = self.dev.descriptor.get().map_or(0, |d| d.serial); + if index == 0 { + return Ok(Serial::Absent); + } + let Ok(languages) = self.dev.control(Setup::get_descriptor(3, 0, 255)).await? else { return Ok(Serial::Unread) }; + let Some(language) = identity::first_language(&languages) else { return Ok(Serial::Unread) }; + let ask = Setup { index: language, ..Setup::get_descriptor(3, index, 255) }; + Ok(match self.dev.control(ask).await? { + Ok(bytes) => Serial::from_descriptor(&bytes), + Err(_) => Serial::Unread, + }) + } + + /// TEST UNIT READY on a budget, INQUIRY and READ CAPACITY + /// (`toyos_scsi::BringUp`). + async fn bring_up(&mut self, say: &dyn Fn(core::fmt::Arguments<'_>)) -> Result, Dead> { + let wake = self.dev.wake(); + let (mut up, mut ask) = scsi::BringUp::begin(wake.now() + READY_NS); + let mut inquiry = None; + let mut broke = None; + loop { + let small = self.addr(self.io + AT_SMALL); + let heard_bytes: Vec; + let heard = match ask { + scsi::Ask::TestUnitReady => match self.bot(&Cdb::TEST_UNIT_READY, Data::None).await? { + Ok(Bot::Done { .. }) => scsi::Heard::Good, + Ok(Bot::Failed) => scsi::Heard::CheckCondition, + Err(b) => { + broke = Some(b); + scsi::Heard::Broke + } + }, + scsi::Ask::RequestSense => scsi::Heard::Sense(self.sense().await?), + scsi::Ask::Recover => { + let b = broke.take().expect("a break to recover from"); + let back = self.climb(b, false).await?; + if self.asked { + return Ok(None); + } + scsi::Heard::Recovered { offline: !back } + } + scsi::Ask::Read(query) => { + let len = query.allocation() as u32; + match self.command(&query.cdb(), Data::In { at: small, len }).await? { + Reply::Ok { delivered } => { + heard_bytes = (0..len as usize).map(|i| self.dev.read8(self.io + AT_SMALL + i)).collect(); + scsi::Heard::Data { bytes: &heard_bytes, delivered } + } + _ if self.asked => return Ok(None), + _ => scsi::Heard::Unanswered, + } + } + }; + match up.heard(heard, wake.now()) { + scsi::Next::Ask(next, a) => (up, ask) = (next, a), + scsi::Next::Disk(next, said, a) => { + inquiry = Some(said); + (up, ask) = (next, a); + } + scsi::Next::Up(scsi::Up::Ready(geometry)) => { + return Ok(Some((geometry, inquiry.expect("INQUIRY is read before the capacity")))); + } + scsi::Next::Up(scsi::Up::Unready { sense, offline }) => { + say(format_args!("it never said it was ready (last sense {sense}, offline: {offline}); not served")); + return Ok(None); + } + scsi::Next::Up(scsi::Up::Refused(why)) => { + say(format_args!("not served: {why:?}")); + return Ok(None); + } + } + } + } + + /// The table, as `Usb::table` reads it, its sectors fetched by replaying + /// the read until it asks for none it has not been given. + async fn table(&mut self, geometry: &Geometry) -> Result { + let per = HOST_BLOCK / geometry.sector_bytes(); + let mut cache = Cache { + lba_bytes: geometry.sector_bytes(), + lbas: geometry.blocks() * u64::from(per), + per: NonZeroU64::new(u64::from(per)).expect("at least one sector a block"), + have: BTreeMap::new(), + missed: Vec::new(), + }; + let mut reads = 0; + loop { + cache.missed.clear(); + let table = (self.dev.usb.table)(&mut cache); + let missed = core::mem::take(&mut cache.missed); + if missed.is_empty() || reads >= TABLE_READS { + return Ok(table); + } + for lba in missed { + let block = lba / u64::from(per); + if cache.have.contains_key(&(block * u64::from(per))) { + continue; + } + reads += 1; + let at = self.addr(self.block); + let cdb = Cdb::transfer(false, (block * u64::from(per)) as u32, per as u16); + let read = self.command(&cdb, Data::In { at, len: HOST_BLOCK }).await?; + for s in 0..u64::from(per) { + let first = (s as usize) * geometry.sector_bytes() as usize; + let sector = match read { + Reply::Ok { delivered } if delivered == HOST_BLOCK => Some( + (0..geometry.sector_bytes() as usize).map(|i| self.dev.read8(self.block + first + i)).collect(), + ), + _ => None, + }; + cache.have.insert(block * u64::from(per) + s, sector); + } + if self.asked { + return Ok(table); + } + } + } + } + + /// Serve disk `disk`'s queue until the device leaves or the ladder asks + /// its port for something. + async fn work(&mut self, disk: u32) -> Result<(), Dead> { + let usb = self.dev.usb; + let disks = &usb.disks; + loop { + let next = exec::until(self.dev.wake(), || { + if self.dev.leaving.get() { + return Some(None); + } + disks.disks.borrow().get(&disk).and_then(|d| d.work.front().copied()).map(Some) + }) + .await; + let Some(work) = next else { return Ok(()) }; + let geometry = disks.disks.borrow()[&disk].geometry; + let (ok, status, write) = match work { + Work::Io { write, block, blocks, at, .. } => { + let (ok, status) = self.io(&geometry, write, block, blocks, at).await?; + (ok, status, write) + } + Work::Flush { .. } => { + let (ok, status, no_cache) = match scsi::flushed(self.command(&Cdb::SYNCHRONIZE_CACHE, Data::None).await?) { + scsi::Flushed::Emptied => (true, 0, false), + scsi::Flushed::NoCache => (true, 0, true), + scsi::Flushed::Refused(sense) => (false, status_of(sense), false), + scsi::Flushed::Ended(_) => (false, 0, false), + }; + if let Some(d) = disks.disks.borrow_mut().get_mut(&disk) { + if ok { + d.debt.flushed(); + } + d.no_cache |= no_cache; + } + (ok, status, false) + } + }; + if self.asked || self.dev.leaving.get() { + // Unanswered: it is issued again to the device that comes back, + // or answered with the disk if none does. + if self.dev.ask.get() == Some(Ask::Reenumerate) + && let Some(d) = disks.disks.borrow_mut().get_mut(&disk) + { + d.reset_at = Some(usb.wake.now()); + } + return Ok(()); + } + let mut all = disks.disks.borrow_mut(); + let d = all.get_mut(&disk).expect("served"); + d.work.pop_front(); + if write && ok { + d.debt.wrote(); + } + drop(all); + disks.event(Event::Answered { disk, token: work.token(), write, ok, status }); + } + } + + /// One read or write, in as many READ(10)s or WRITE(10)s as it takes. + async fn io(&mut self, geometry: &Geometry, write: bool, block: u64, blocks: u32, at: u64) -> Result<(bool, u32), Dead> { + let mut transfer = match scsi::Transfer::new(block, blocks, write, geometry, toyos_blockring::MAX_REQUEST_BLOCKS) { + Ok(t) => t, + Err(past) => { + self.dev.usb.say(format_args!("a transfer past the disk's end refused: {past:?}")); + return Ok((false, 0)); + } + }; + while let Some(batch) = transfer.next() { + let at = at + batch.offset as u64; + let len = batch.bytes as u32; + let data = if write { Data::Out { at, len } } else { Data::In { at, len } }; + let reply = self.command(&batch.cdb, data).await?; + match transfer.answered(&batch, reply) { + scsi::Moved::Whole => {} + scsi::Moved::Refused(sense) => return Ok((false, status_of(sense))), + _ => return Ok((false, 0)), + } + } + Ok((true, 0)) + } + + fn next_tag(&mut self) -> u32 { + self.tag = self.tag.wrapping_add(1); + self.tag + } + + /// A command with the ladder behind it. + async fn command(&mut self, cdb: &Cdb, data: Data) -> Result { + loop { + if self.asked || self.dev.leaving.get() { + return Ok(Reply::Broken); + } + match self.bot(cdb, data).await? { + Ok(Bot::Done { delivered }) => { + self.run.over(); + return Ok(Reply::Ok { delivered }); + } + Ok(Bot::Failed) => { + self.run.over(); + return Ok(Reply::Refused(self.sense().await?)); + } + Err(Broke::Gone { .. }) => return Ok(Reply::Broken), + Err(broke) => { + if !self.climb(broke, matches!(data, Data::Out { .. })).await? { + return Ok(Reply::Broken); + } + } + } + } + } + + /// REQUEST SENSE, read and judged; [`Sense::NONE`] where it says nothing + /// this driver reads. + async fn sense(&mut self) -> Result { + let at = self.addr(self.io + AT_SMALL); + let delivered = match self.bot(&Cdb::REQUEST_SENSE, Data::In { at, len: SENSE_BYTES as u32 }).await? { + Ok(Bot::Done { delivered }) => delivered, + _ => return Ok(Sense::NONE), + }; + let mut bytes = [0u8; SENSE_BYTES]; + for (i, b) in bytes.iter_mut().enumerate() { + *b = self.dev.read8(self.io + AT_SMALL + i); + } + Ok(Sense::of(&bytes, delivered).unwrap_or_else(|why| { + self.dev.usb.say(format_args!("its sense is refused: {why:?}")); + Sense::NONE + })) + } + + /// The ladder's next rung for `broke`: `true` once the transport is + /// verified back, `false` once the device is asked away. + async fn climb(&mut self, broke: Broke, data_out: bool) -> Result { + let mut left = broke.left(data_out); + self.dev.usb.say(format_args!("a Bulk-Only round trip broke: {broke:?}")); + loop { + let climb = self.run.broke(left); + match climb.rung { + Rung::ClassReset => { + if self.class_reset().await? { + let tag_check = self.bot(&Cdb::TEST_UNIT_READY, Data::None).await?; + if tag_check.is_ok() { + self.dev.usb.say(format_args!("Reset Recovery brought the transport back")); + return Ok(true); + } + } + left = Left::Elsewhere; + } + rung @ (Rung::PortReset | Rung::Offline) => { + self.dev.usb.say(format_args!("the ladder climbs to its {}", rung.named())); + self.asked = true; + let ask = if rung == Rung::PortReset { Ask::Reenumerate } else { Ask::Offline }; + if ask == Ask::Reenumerate { + self.dev.usb.runs.borrow_mut().insert(self.dev.port, self.run); + } + self.dev.ask.set(Some(ask)); + return Ok(false); + } + } + } + } + + /// Bulk-Only Reset Recovery (§5.3.4): both pipes off their transfers, the + /// class reset and a CLEAR_FEATURE(ENDPOINT_HALT) to each, which zero the + /// device's toggles whether or not either was halted (USB 2.0 §9.4.5), + /// and both endpoints re-created with the controller's zeroed (xHCI 1.2 + /// §4.6.8's note). + async fn class_reset(&mut self) -> Result { + let dev = self.dev; + for ep in [self.bulk_in, self.bulk_out] { + let id = u32::from(dev.slot) << 24 | u32::from(ep.dci()) << 16; + let command = match dev.endpoint_state(ep.dci()) { + crate::recovery::EndpointState::Running => hc::TRB_STOP_ENDPOINT, + crate::recovery::EndpointState::Halted => hc::TRB_RESET_ENDPOINT, + _ => continue, + }; + let done = dev.hc().command(dev.wake(), Trb { control: command | id, ..Trb::default() }).await?; + if !done.succeeded() { + return Ok(false); + } + } + let index = u16::from(self.interface); + let reset = Setup { request_type: 0x21, request: 0xFF, value: 0, index, length: 0 }; + if dev.control(reset).await?.is_err() { + return Ok(false); + } + for ep in [self.bulk_in, self.bulk_out] { + let clear = Setup { request_type: 0x02, request: 1, value: 0, index: u16::from(ep.address), length: 0 }; + if dev.control(clear).await?.is_err() { + return Ok(false); + } + } + Ok(dev.configure(&[self.bulk_in, self.bulk_out], Configure::Again).await?.is_ok()) + } + + /// One round trip, as `bot::RoundTrip` orders it. + async fn bot(&mut self, cdb: &Cdb, data: Data) -> Result>, Dead> { + let tag = self.next_tag(); + let (mut trip, mut act) = RoundTrip::begin(tag, data.len(), cdb); + loop { + let answer = match act { + bot::Act::Command => { + self.dev.write_bytes(self.io, &bot::cbw(tag, data.len(), cdb)); + self.transfer(Pipe::Out, &[(self.addr(self.io), CBW_LEN as u32)]).await? + } + bot::Act::Data(pipe) => { + let (at, len) = match data { + Data::In { at, len } | Data::Out { at, len } => (at, len), + Data::None => unreachable!("no data phase is asked of a command without one"), + }; + self.transfer(pipe, &pieces(at, len)).await? + } + bot::Act::Status => { + self.dev.write_bytes(self.io + AT_CSW, &[0; 16]); + self.transfer(Pipe::In, &[(self.addr(self.io + AT_CSW), CSW_LEN as u32)]).await? + } + bot::Act::Restart { pipe, .. } => Answer::Restarted(self.dev.restart(self.dci(pipe), None).await?.is_ok()), + }; + match trip.answered(answer) { + bot::Next::Act(next, a) => (trip, act) = (next, a), + bot::Next::Csw(due) => { + let mut csw = [0u8; CSW_LEN]; + for (i, b) in csw.iter_mut().enumerate() { + *b = self.dev.read8(self.io + AT_CSW + i); + } + return Ok(due.judge(&csw)); + } + bot::Next::Broke(broke) => return Ok(Err(broke)), + } + } + } + + /// One bulk transfer of `pieces`, answered, silent or gone; a silent one + /// is taken back from the controller before it is answered. + async fn transfer(&mut self, pipe: Pipe, pieces: &[(u64, u32)]) -> Result, Dead> { + if self.dev.leaving.get() { + return Ok(Answer::Gone); + } + let dci = self.dci(pipe); + let total: u32 = pieces.iter().map(|(_, len)| len).sum(); + let max_packet = match pipe { + Pipe::In => self.in_packet, + Pipe::Out => self.out_packet, + }; + let trbs = hc::normal_td(pieces, max_packet); + let mut op = self.dev.submit(dci, &trbs); + let deadline = self.dev.wake().now() + COMMAND_NS; + Ok(match op.answer(self.dev.wake(), deadline).await { + Some(Done::Gone) => Answer::Gone, + Some(Done::Event { code, moved, .. }) => Answer::Moved { code, residue: total.saturating_sub(moved) }, + None => { + let _ = self.dev.restart(dci, Some(op)).await?; + Answer::Silent(Silent) + } + }) + } +} + +/// `len` bytes from `at` as TRB-sized pieces, none crossing a 64 KiB +/// boundary (xHCI 1.2 §6.4.1). +fn pieces(at: u64, len: u32) -> Vec<(u64, u32)> { + let mut out = Vec::new(); + let (mut at, mut left) = (at, u64::from(len)); + while left > 0 { + let room = TRB_MOST - (at % TRB_MOST); + let take = room.min(left); + out.push((at, take as u32)); + at += take; + left -= take; + } + out +} + +fn status_of(sense: Sense) -> u32 { + u32::from(sense.key) << 16 | u32::from(sense.asc) << 8 | u32::from(sense.ascq) +} + +/// The disk's sectors as a table read asks for them: those fetched, and +/// every one asked for that was not. +struct Cache { + lba_bytes: u32, + lbas: u64, + per: NonZeroU64, + /// `None` for a sector whose read failed. + have: BTreeMap>>, + missed: Vec, +} + +impl toyos_gpt::Sectors for Cache { + fn lba_bytes(&self) -> u32 { + self.lba_bytes + } + fn lba_count(&self) -> u64 { + self.lbas + } + fn lba_count_granularity(&self) -> NonZeroU64 { + self.per + } + fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool { + match self.have.get(&lba) { + Some(Some(sector)) => { + buf.copy_from_slice(sector); + true + } + Some(None) => false, + None => { + self.missed.push(lba); + false + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn no_piece_crosses_a_64_kib_boundary() { + assert_eq!(pieces(0x1_0000, 0x2_0000), [(0x1_0000, 0x1_0000), (0x2_0000, 0x1_0000)]); + assert_eq!(pieces(0xF000, 0x3000), [(0xF000, 0x1000), (0x1_0000, 0x2000)]); + for at in [0u64, 0x200, 0xFFFF, 0x1_2345] { + for len in [1u32, 0x1000, 0x2_0000] { + let p = pieces(at, len); + assert_eq!(p.iter().map(|(_, l)| l).sum::(), len); + assert!(p.iter().all(|&(a, l)| a / TRB_MOST == (a + u64::from(l) - 1) / TRB_MOST)); + } + } + } +} diff --git a/lib/xhci/src/port.rs b/system/usbd/xhci/src/port.rs similarity index 87% rename from lib/xhci/src/port.rs rename to system/usbd/xhci/src/port.rs index 00b72ed31fc..bd8b9aaaf5b 100644 --- a/lib/xhci/src/port.rs +++ b/system/usbd/xhci/src/port.rs @@ -21,16 +21,22 @@ pub type Nanos = u64; /// TCON_RST. pub const DEBOUNCE_NS: Nanos = 100_000_000; -/// How long a scan on which *nothing at all* has connected keeps looking, from -/// the instant the ports were powered. +/// How long after its port is powered a device present at the driver's start +/// has to make itself seen, by the protocol the port speaks; a port still +/// reading no connect past it is empty, and a device that arrives later is a +/// hot-plug. /// -/// Debounce alone can't tell an empty bus from a device still connecting: both -/// read as already-settled until something changes. -pub const EMPTY_BUS_NS: Nanos = 1_000_000_000; - -/// How long the slow-connect injection reports an empty root hub, from that same -/// port power. -pub const SLOW_CONNECT_NS: Nanos = 300_000_000; +/// USB 2: TSIGATT, the most a device may take to signal attach after VBUS is +/// valid (USB 2.0 §7.1.7.3, Table 7-14). USB 3: tPollingLFPSTimeout, the most +/// Polling.LFPS waits for its partner's LFPS once a receiver is detected +/// (USB 3.2 §7.5.4.3), past which the link trains no further. +pub fn attach_window(usb3: bool) -> Nanos { + if usb3 { + 360_000_000 + } else { + 100_000_000 + } +} /// How long a reset may take before the port is given up on. /// @@ -42,9 +48,9 @@ pub const RESET_DEADLINE_NS: Nanos = 2_000_000_000; /// Which reset a connected port needs before anything can be enumerated on it, /// or `None` when its link is already up and there is nothing to do. /// -/// **The one place that question is answered**: the hot-plug machine acts on -/// this answer, and the boot scan acts on the same one through -/// [`inherited_reset`], which only says what its `None` costs at bring-up. +/// **The one place that question is answered**: a port's task acts on this +/// answer, and a port found connected at bring-up on the same one through +/// [`inherited_reset`], which only says what its `None` costs there. pub fn reset_needed(protocol: Option, portsc: Portsc) -> Option { if protocol != Some(Protocol::Usb3) { // USB2, or a port the controller did not describe. A reset is how a @@ -63,7 +69,7 @@ pub fn reset_needed(protocol: Option, portsc: Portsc) -> Option Some(if portsc.link_state() == LinkState::Inactive { Reset::Warm } else { Reset::Hot }) } -/// Which reset a device already connected when this kernel brings its +/// Which reset a device already connected when this driver brings its /// controller up needs, given [`reset_needed`]'s answer for its port: /// **always one**, and on a link that reads trained the warm one. /// @@ -73,12 +79,12 @@ pub fn reset_needed(protocol: Option, portsc: Portsc) -> Option /// nothing; SET_ADDRESS to a device in the Configured state is not specified /// (USB 2.0 §9.4.6), and every enumeration the USB specification describes /// begins with a port reset (USB 2.0 §9.1.2). So nothing is asked of it until -/// its port has been reset and this kernel has enumerated it from its Default +/// its port has been reset and this driver has enumerated it from its Default /// state. A trained link gets [`Reset::Warm`] because its link state is as /// unknown as its protocol state, and a warm reset does everything a hot one -/// does and also retrains the link (§4.19.5.1). A link this kernel watched +/// does and also retrains the link (§4.19.5.1). A link this driver watched /// train is not this question: its device came up from power-on, and the -/// hot-plug machine acts on [`reset_needed`] unchanged. +/// port's task acts on [`reset_needed`] unchanged. pub fn inherited_reset(needed: Option) -> Reset { needed.unwrap_or(Reset::Warm) } @@ -106,9 +112,7 @@ pub fn reset_write(reset: Reset, portsc: Portsc) -> portsc::Write { /// The write an enumeration opens with: the reset flags it consumed, and for a /// warm completion the retrain's own connect edge (§4.19.5.1), which left set -/// reads as a replug and cancels the enumeration it belongs to. Here rather -/// than at either caller, because a simulator whose acknowledge is a second -/// copy of the driver's certifies a driver nobody ships. +/// reads as a replug and cancels the enumeration it belongs to. pub fn enumeration_ack(after: Option, portsc: Portsc) -> portsc::Write { let ack = portsc.neutral().acknowledging_reset(portsc); match after { @@ -154,8 +158,8 @@ impl GaveUp { /// What a completed reset means for the port — **the one place that question /// is answered**, for [`reset_needed`]'s reason: §4.19.5's failure signature -/// (PRC set, the port still disabled) must route the boot scan and the -/// hot-plug machine to the same §4.19.5.1 escalation. +/// (PRC set, the port still disabled) must route every reset to the same +/// §4.19.5.1 escalation. pub fn reset_outcome(kind: Reset, protocol: Option, portsc: Portsc) -> ResetOutcome { if portsc.enabled() { return ResetOutcome::Enumerate; @@ -287,27 +291,6 @@ enum Work { GivenUp, } -/// A deliberate defect, compiled only for the negative gates. -/// -/// Each names one decision below that every positive scenario in the simulator -/// would still pass without. A gate that cannot fail proves nothing, so the -/// simulator turns each of these on in turn and requires the run to go red. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] -pub enum Flaw { - #[default] - None, - /// Compare CCS against what the driver believes and ignore CSC. - IgnoreConnectChange, - /// Clear the change flags before deciding what they meant. - AcknowledgeBeforeDeciding, - /// Write back the whole word that was read. - WriteBackWhatWasRead, - /// Restart the debounce clock on every observation. - RestartDebounce, - /// Wait for a reset for as long as it takes. - NoResetDeadline, -} - /// One root-hub port, as the driver believes it. #[derive(Clone, Copy, Debug)] pub struct PortState { @@ -324,8 +307,6 @@ pub struct PortState { /// Unknown is driven the USB2 way, which is what every port got before the /// Supported Protocol capability was read at all. protocol: Option, - #[cfg(feature = "flaws")] - flaw: Flaw, } impl Default for PortState { @@ -340,8 +321,6 @@ impl PortState { slot: None, work: Work::Settled, protocol: None, - #[cfg(feature = "flaws")] - flaw: Flaw::None, }; /// The slot this port's device holds, which is what a teardown gives back. @@ -442,10 +421,6 @@ impl PortState { /// controller may act on PR and drop the write-1-to-clear bits beside /// it: QEMU 11.1's `qemu-xhci` returns from a PORTSC write once it has /// reset the port. - /// - /// For a driver that steps every port with [`Self::step`] from bring-up - /// on: a boot scan that waits in place answers the same question through - /// [`inherited_reset`] and keeps no machine while it does. pub fn adopt(&mut self, portsc: Portsc, now: Nanos) -> (Reset, [portsc::Write; 2]) { let kind = inherited_reset(reset_needed(self.protocol, portsc)); self.work = Work::Resetting { until: now + RESET_DEADLINE_NS, kind }; @@ -457,34 +432,10 @@ impl PortState { Step::GaveUp(why) } - #[cfg(feature = "flaws")] - pub fn with_flaw(flaw: Flaw) -> Self { - Self { flaw, ..Self::EMPTY } - } - - /// Whether this driver was staged with `flaw`: a flawed acknowledge is - /// flawed at the simulator's effect sites too, not only in the machine. - #[cfg(feature = "flaws")] - pub fn has_flaw(&self, flaw: Flaw) -> bool { - self.flawed(flaw) - } - - #[cfg(feature = "flaws")] - fn flawed(&self, flaw: Flaw) -> bool { - self.flaw == flaw - } - - /// Always false in a kernel build: the field a flaw would be read from does - /// not exist there, so no production path can reach one. - #[cfg(not(feature = "flaws"))] - fn flawed(&self, _flaw: Flaw) -> bool { - false - } - /// What to do about this port, given what it reads now. pub fn step(&mut self, portsc: Portsc, now: Nanos) -> Step<'_> { let connected = portsc.connected(); - let replugged = portsc.connect_changed() && !self.flawed(Flaw::IgnoreConnectChange); + let replugged = portsc.connect_changed(); // A step taken from inside an effect this port is already having done // to it. Nothing to decide — the caller has not finished the last @@ -509,7 +460,7 @@ impl PortState { ResetOutcome::GaveUp(why) => return self.give_up(why), } } - if now < until || self.flawed(Flaw::NoResetDeadline) { + if now < until { return Step::Wait(until); } // **A hot reset a USB3 port would not take is what a warm reset @@ -523,10 +474,6 @@ impl PortState { return self.give_up(GaveUp::never_finished(kind)); } - if self.flawed(Flaw::AcknowledgeBeforeDeciding) && portsc.any_change() { - return Step::Write(portsc.neutral().acknowledging(portsc)); - } - // **Ahead of the acknowledge, because the acknowledge is what destroys // the evidence.** The port reads connected and the driver already // believed it was, so nothing below would act — but CSC says the @@ -539,15 +486,7 @@ impl PortState { } if portsc.any_change() { - #[cfg(feature = "flaws")] - let write = if self.flawed(Flaw::WriteBackWhatWasRead) { - portsc::Write::whole_word(portsc) - } else { - portsc.neutral().acknowledging(portsc) - }; - #[cfg(not(feature = "flaws"))] - let write = portsc.neutral().acknowledging(portsc); - return Step::Write(write); + return Step::Write(portsc.neutral().acknowledging(portsc)); } let held = match self.work { @@ -565,10 +504,6 @@ impl PortState { self.work = Work::Settled; return Step::Idle; } - Work::Debouncing { .. } if self.flawed(Flaw::RestartDebounce) => { - self.work = Work::Debouncing { at: now }; - 0 - } Work::Debouncing { at } => now.saturating_sub(at), Work::Resetting { .. } | Work::Working(_) => unreachable!("returned above"), }; @@ -596,7 +531,7 @@ mod tests { Portsc::from_raw(1 | (u32::from(enabled) << 1) | (pls << 5) | (1 << 9) | (4 << 10)) } - /// What the boot scan does with a port it finds connected: one + /// What bring-up does with a port it finds connected: one /// [`reset_needed`], read through [`inherited_reset`]. fn inherited(protocol: Option, portsc: Portsc) -> Reset { inherited_reset(reset_needed(protocol, portsc)) diff --git a/lib/xhci/src/portsc.rs b/system/usbd/xhci/src/portsc.rs similarity index 98% rename from lib/xhci/src/portsc.rs rename to system/usbd/xhci/src/portsc.rs index ed185043f58..a667df3c185 100644 --- a/lib/xhci/src/portsc.rs +++ b/system/usbd/xhci/src/portsc.rs @@ -262,11 +262,10 @@ impl Write { self.0 } - /// The write a driver makes when it hands the register back what it read. - /// Compiled only for the negative gate that requires this to be wrong. - #[cfg(feature = "flaws")] - pub const fn whole_word(read: Portsc) -> Self { - Self(read.0) + /// A word a test's model was handed, to judge it as the driver's write. + #[cfg(test)] + pub const fn of(raw: u32) -> Self { + Self(raw) } } diff --git a/lib/xhci/src/protocol.rs b/system/usbd/xhci/src/protocol.rs similarity index 100% rename from lib/xhci/src/protocol.rs rename to system/usbd/xhci/src/protocol.rs diff --git a/lib/xhci/src/recovery.rs b/system/usbd/xhci/src/recovery.rs similarity index 97% rename from lib/xhci/src/recovery.rs rename to system/usbd/xhci/src/recovery.rs index b1e3cc32541..45d17e35d24 100644 --- a/lib/xhci/src/recovery.rs +++ b/system/usbd/xhci/src/recovery.rs @@ -8,11 +8,8 @@ //! State Error whenever the break was not a halt, which the laptop answered //! twice before calling its own boot disk offline. //! -//! The sequence is here and its effects are the driver's, because it has two -//! drivers: a blocking loop for a disk's bulk pair, which runs on a faulting -//! thread where waiting is somebody's own time, and a stepped one for a HID -//! interrupt endpoint, which runs at the top of a scheduler pass where it is -//! everybody's. +//! The sequence is here and its effects are the driver's +//! (`usb::Device::restart`). /// What the controller believes about one endpoint, out of the Endpoint State /// field of its *output* context (xHCI 1.2 Table 6-8, dword 0 bits 2:0). diff --git a/system/usbd/xhci/src/stub/devices.rs b/system/usbd/xhci/src/stub/devices.rs new file mode 100644 index 00000000000..a3b7b998eda --- /dev/null +++ b/system/usbd/xhci/src/stub/devices.rs @@ -0,0 +1,323 @@ +//! The devices the stub's ports hold, each written from its class's +//! specification: a HID keyboard (HID 1.11, its report descriptor Appendix +//! E.6's), and a stick, SCSI over Bulk-Only (BOT 1.0, SPC-4, SBC-3) on a disk +//! in memory. + +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::rc::Rc; + +use super::{Function, Reply}; + +/// HID 1.11 Appendix E.6, the boot keyboard's report descriptor. +pub const E6_KEYBOARD: &[u8] = &[ + 0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, 0x05, 0x07, 0x19, 0xE0, 0x29, 0xE7, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, + 0x08, 0x81, 0x02, 0x95, 0x01, 0x75, 0x08, 0x81, 0x01, 0x95, 0x05, 0x75, 0x01, 0x05, 0x08, 0x19, 0x01, 0x29, 0x05, + 0x91, 0x02, 0x95, 0x01, 0x75, 0x03, 0x91, 0x01, 0x95, 0x06, 0x75, 0x08, 0x15, 0x00, 0x25, 0x65, 0x05, 0x07, 0x19, + 0x00, 0x29, 0x65, 0x81, 0x00, 0xC0, +]; + +fn device_descriptor(packet: u8, vendor: u16, product: u16, serial: u8) -> Vec { + let [v0, v1] = vendor.to_le_bytes(); + let [p0, p1] = product.to_le_bytes(); + vec![18, 1, 0x00, 0x02, 0, 0, 0, packet, v0, v1, p0, p1, 0x00, 0x01, 0, 0, serial, 1] +} + +fn string(text: &str) -> Vec { + let mut out = vec![0, 3]; + for unit in text.encode_utf16() { + out.extend(unit.to_le_bytes()); + } + out[0] = out.len() as u8; + out +} + +/// The requests every device answers alike (USB 2.0 §9.4): its descriptors, +/// SET_CONFIGURATION and CLEAR_FEATURE. +fn standard(setup: [u8; 8], device: &[u8], config: &[u8], serial: Option<&str>) -> Option { + let (kind, request, value) = (setup[0], setup[1], u16::from_le_bytes([setup[2], setup[3]])); + let length = usize::from(u16::from_le_bytes([setup[6], setup[7]])); + let answer = |bytes: Vec| Some(Reply::Data(bytes.into_iter().take(length).collect())); + match (kind, request, value >> 8, value & 0xFF) { + (0x80, 6, 1, _) => answer(device.to_vec()), + (0x80, 6, 2, _) => answer(config.to_vec()), + (0x80, 6, 3, 0) => answer(vec![4, 3, 0x09, 0x04]), + (0x80, 6, 3, 3) => serial.map(string).map(Reply::Data).map(|r| match r { + Reply::Data(b) => Reply::Data(b.into_iter().take(length).collect()), + other => other, + }), + (0x00, 9, ..) | (0x02, 1, ..) => Some(Reply::Data(Vec::new())), + _ => None, + } +} + +/// A keyboard whose reports a test pushes. +pub struct Keyboard { + pub reports: Rc>>>, + /// Every Setup packet it was sent. + pub requests: Rc>>, + /// Whether it answers SET_IDLE, which is optional outside the boot + /// subclass (HID 1.11 §7.2). + pub stalls_idle: bool, +} + +impl Keyboard { + pub fn new() -> (Self, Rc>>>) { + let reports = Rc::new(RefCell::new(VecDeque::new())); + (Self { reports: reports.clone(), requests: Rc::default(), stalls_idle: false }, reports) + } + + fn config() -> Vec { + let [l0, l1] = (E6_KEYBOARD.len() as u16).to_le_bytes(); + let mut c = vec![9, 2, 34, 0, 1, 1, 0, 0xA0, 50]; + c.extend([9, 4, 0, 0, 1, 3, 1, 1, 0]); + c.extend([9, 0x21, 0x11, 0x01, 0, 1, 0x22, l0, l1]); + c.extend([7, 5, 0x81, 3, 8, 0, 10]); + c + } +} + +impl Function for Keyboard { + fn control(&mut self, setup: [u8; 8], _: &[u8]) -> Reply { + self.requests.borrow_mut().push(setup); + let length = usize::from(u16::from_le_bytes([setup[6], setup[7]])); + if let Some(reply) = standard(setup, &device_descriptor(8, 0x1209, 0x0001, 0), &Self::config(), None) { + return reply; + } + match (setup[0], setup[1], setup[3]) { + (0x81, 6, 0x22) => Reply::Data(E6_KEYBOARD.iter().copied().take(length).collect()), + (0x21, 0x0A, _) if self.stalls_idle => Reply::Stall, + (0x21, 0x0A | 0x0B, _) => Reply::Data(Vec::new()), + _ => Reply::Stall, + } + } + + fn transfer(&mut self, address: u8, _: Option<&[u8]>, _: usize) -> Reply { + assert_eq!(address, 0x81, "a keyboard has one endpoint"); + match self.reports.borrow_mut().pop_front() { + Some(report) => Reply::Data(report), + None => Reply::Wait, + } + } +} + +/// What a stick's bulk pipes do, which a test sets. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +pub enum Pipes { + #[default] + Answer, + /// The Bulk-In endpoint never answers: every IN transfer waits forever. + SilentIn, + /// The Bulk-Out endpoint never answers. + SilentOut, + /// The Bulk-In endpoint halts with its event lost, and CLEAR_FEATURE(HALT) + /// stalls. + HaltUnannounced, +} + +enum Bot { + Idle, + DataIn { data: Vec, csw: [u8; 13] }, + DataOut { lba: u64, len: usize, csw: [u8; 13] }, + Status([u8; 13]), +} + +/// A stick: one logical unit of 512-byte sectors over Bulk-Only. +pub struct Stick { + pub disk: Rc>>, + pub pipes: Rc>, + pub serial: String, + /// CBWs it took, by operation code. + pub commands: Rc>>, + state: Bot, + sense: (u8, u8, u8), +} + +const SECTOR: usize = 512; + +impl Stick { + pub fn new(disk: Vec, serial: &str) -> Self { + Self { + disk: Rc::new(RefCell::new(disk)), + pipes: Rc::default(), + serial: serial.into(), + commands: Rc::default(), + state: Bot::Idle, + sense: (0, 0, 0), + } + } + + fn config() -> Vec { + let mut c = vec![9, 2, 32, 0, 1, 1, 0, 0xC0, 50]; + c.extend([9, 4, 0, 0, 2, 0x08, 0x06, 0x50, 0]); + c.extend([7, 5, 0x81, 2, 0x00, 0x02, 0]); + c.extend([7, 5, 0x02, 2, 0x00, 0x02, 0]); + c + } + + fn csw(tag: [u8; 4], residue: u32, status: u8) -> [u8; 13] { + let mut csw = [0u8; 13]; + csw[..4].copy_from_slice(b"USBS"); + csw[4..8].copy_from_slice(&tag); + csw[8..12].copy_from_slice(&residue.to_le_bytes()); + csw[12] = status; + csw + } + + /// The command block `cbw` carries, run (SPC-4, SBC-3). + fn command(&mut self, cbw: &[u8]) -> Bot { + let tag = [cbw[4], cbw[5], cbw[6], cbw[7]]; + let length = u32::from_le_bytes([cbw[8], cbw[9], cbw[10], cbw[11]]) as usize; + let cdb = &cbw[15..31]; + self.commands.borrow_mut().push(cdb[0]); + let sectors = self.disk.borrow().len() / SECTOR; + let be32 = |at: usize| u32::from_be_bytes([cdb[at], cdb[at + 1], cdb[at + 2], cdb[at + 3]]); + let reply_in = |mut data: Vec, sense: &mut (u8, u8, u8)| { + *sense = (0, 0, 0); + data.truncate(length); + let residue = (length - data.len()) as u32; + Bot::DataIn { data, csw: Self::csw(tag, residue, 0) } + }; + match cdb[0] { + 0x00 | 0x35 => { + self.sense = (0, 0, 0); + Bot::Status(Self::csw(tag, 0, 0)) + } + 0x03 => { + let mut sense = vec![0u8; 18]; + sense[0] = 0x70; + sense[2] = self.sense.0; + sense[7] = 10; + sense[12] = self.sense.1; + sense[13] = self.sense.2; + reply_in(sense, &mut self.sense) + } + 0x12 => { + let mut data = vec![0u8; 36]; + data[4] = 31; + data[8..16].copy_from_slice(b"ToyStub "); + data[16..32].copy_from_slice(b"Stick "); + data[32..36].copy_from_slice(b"1.00"); + reply_in(data, &mut self.sense) + } + 0x25 => { + let mut data = ((sectors - 1) as u32).to_be_bytes().to_vec(); + data.extend((SECTOR as u32).to_be_bytes()); + reply_in(data, &mut self.sense) + } + 0x28 => { + let (lba, count) = (be32(2) as usize, usize::from(u16::from_be_bytes([cdb[7], cdb[8]]))); + let data = self.disk.borrow()[lba * SECTOR..(lba + count) * SECTOR].to_vec(); + reply_in(data, &mut self.sense) + } + 0x2A => { + let count = usize::from(u16::from_be_bytes([cdb[7], cdb[8]])); + Bot::DataOut { lba: u64::from(be32(2)), len: count * SECTOR, csw: Self::csw(tag, 0, 0) } + } + _ => { + // ILLEGAL REQUEST, INVALID COMMAND OPERATION CODE. + self.sense = (0x05, 0x20, 0x00); + Bot::Status(Self::csw(tag, length as u32, 1)) + } + } + } +} + +impl Function for Stick { + fn reset(&mut self) { + self.state = Bot::Idle; + } + + fn control(&mut self, setup: [u8; 8], _: &[u8]) -> Reply { + let device = device_descriptor(64, 0x1209, 0x0002, 3); + if (setup[0], setup[1], self.pipes.get()) == (0x02, 1, Pipes::HaltUnannounced) { + return Reply::Stall; + } + if let Some(reply) = standard(setup, &device, &Self::config(), Some(&self.serial)) { + return reply; + } + match (setup[0], setup[1]) { + // Bulk-Only Mass Storage Reset (§3.1): ready for the next CBW. + (0x21, 0xFF) => { + self.state = Bot::Idle; + Reply::Data(Vec::new()) + } + _ => Reply::Stall, + } + } + + fn transfer(&mut self, address: u8, out: Option<&[u8]>, max: usize) -> Reply { + match (address, self.pipes.get()) { + (0x81, Pipes::SilentIn) | (0x02, Pipes::SilentOut) => return Reply::Wait, + (0x81, Pipes::HaltUnannounced) => return Reply::StallUnannounced, + _ => {} + } + match (address, out, std::mem::replace(&mut self.state, Bot::Idle)) { + (0x02, Some(cbw), Bot::Idle) if cbw.len() == 31 && &cbw[..4] == b"USBC" => { + self.state = self.command(cbw); + Reply::Data(Vec::new()) + } + (0x02, Some(data), Bot::DataOut { lba, len, csw }) => { + let take = data.len().min(len); + let at = lba as usize * SECTOR; + self.disk.borrow_mut()[at..at + take].copy_from_slice(&data[..take]); + self.state = Bot::Status(csw); + Reply::Data(Vec::new()) + } + (0x81, None, Bot::DataIn { data, csw }) => { + self.state = Bot::Status(csw); + Reply::Data(data.into_iter().take(max).collect()) + } + (0x81, None, Bot::Status(csw)) => Reply::Data(csw.to_vec()), + (_, _, state) => { + // Out of step: §6.6.1 has the device stall what it was not + // expecting. + self.state = state; + Reply::Stall + } + } + } +} + +/// A disk of `sectors` 512-byte sectors carrying a GPT (UEFI 2.10 §5.3) that +/// names one partition, `unique`, from LBA `first` for `count` sectors. +pub fn gpt_disk(sectors: u64, unique: [u8; 16], first: u64, count: u64) -> Vec { + let mut disk = vec![0u8; sectors as usize * SECTOR]; + let mut entries = vec![0u8; 128 * 128]; + entries[..16].copy_from_slice(&[0x11; 16]); + entries[16..32].copy_from_slice(&unique); + entries[32..40].copy_from_slice(&first.to_le_bytes()); + entries[40..48].copy_from_slice(&(first + count - 1).to_le_bytes()); + let entries_crc = toyos_gpt::crc32(&entries); + let last = sectors - 1; + let header = |mine: u64, other: u64, entries_at: u64| { + let mut h = vec![0u8; 92]; + h[..8].copy_from_slice(b"EFI PART"); + h[8..12].copy_from_slice(&0x0001_0000u32.to_le_bytes()); + h[12..16].copy_from_slice(&92u32.to_le_bytes()); + h[24..32].copy_from_slice(&mine.to_le_bytes()); + h[32..40].copy_from_slice(&other.to_le_bytes()); + h[40..48].copy_from_slice(&34u64.to_le_bytes()); + h[48..56].copy_from_slice(&(last - 33).to_le_bytes()); + h[56..72].copy_from_slice(&[0x22; 16]); + h[72..80].copy_from_slice(&entries_at.to_le_bytes()); + h[80..84].copy_from_slice(&128u32.to_le_bytes()); + h[84..88].copy_from_slice(&128u32.to_le_bytes()); + h[88..92].copy_from_slice(&entries_crc.to_le_bytes()); + let crc = toyos_gpt::crc32(&h); + h[16..20].copy_from_slice(&crc.to_le_bytes()); + h + }; + // A protective MBR (§5.2.3), whose one entry covers the disk. + disk[446 + 4] = 0xEE; + disk[446 + 8..446 + 12].copy_from_slice(&1u32.to_le_bytes()); + disk[446 + 12..446 + 16].copy_from_slice(&((sectors - 1).min(u64::from(u32::MAX)) as u32).to_le_bytes()); + disk[510] = 0x55; + disk[511] = 0xAA; + let at = |lba: u64| lba as usize * SECTOR; + disk[at(1)..at(1) + 92].copy_from_slice(&header(1, last, 2)); + disk[at(2)..at(2) + entries.len()].copy_from_slice(&entries); + disk[at(last - 32)..at(last - 32) + entries.len()].copy_from_slice(&entries); + disk[at(last)..at(last) + 92].copy_from_slice(&header(last, 1, last - 32)); + disk +} diff --git a/system/usbd/xhci/src/stub/mod.rs b/system/usbd/xhci/src/stub/mod.rs new file mode 100644 index 00000000000..637f3dcb125 --- /dev/null +++ b/system/usbd/xhci/src/stub/mod.rs @@ -0,0 +1,826 @@ +//! An xHC written from xHCI 1.2, for the driver's host tests: the register +//! file, the command ring, the event ring, transfer rings and the device +//! contexts, with each port's device a model of its class. +//! +//! **The specification is the oracle**, so what the controller does is what +//! the clause it cites says, and where the specification allows a latitude +//! the model takes the one least kind to a driver: a doorbell is acted on +//! only when the test lets time pass ([`Xhc::run`]), an event names only the +//! TRB the clause says it names, and a short packet ends a TD and raises no +//! event for its IOC TRB (§4.10.1.1). +//! +//! Memory is the grant and whatever a test lends at [`LENT_BASE`]; an access +//! outside both is a DMA fault, counted, and every test asserts none. + +pub mod devices; +pub mod port; + +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::rc::Rc; + +use toyos_device_memory::{DmaBuffers, Registers}; + +use crate::exec::Nanos; +use crate::hc::Trb; +use crate::invariants; +use port::FakePort; + +/// Where the grant starts in the controller's address space, and where a +/// test's lent regions do. +pub const GRANT_BASE: u64 = 0x8000_0000; +pub const LENT_BASE: u64 = 0x40_0000_0000; + +const CAPLENGTH: usize = 0x20; +const RTS: usize = 0x1000; +const DB: usize = 0x2000; +const XECP: usize = 0x3000; +const BAR_BYTES: usize = 0x4000; +const SLOTS: usize = 32; + +const USBCMD_RS: u32 = 1; +const USBCMD_HCRST: u32 = 2; +const USBSTS_HCH: u32 = 1; +const CRCR_CA: u32 = 1 << 2; + +const CC_SUCCESS: u32 = 1; +const CC_STALL: u32 = 6; +const CC_SLOTS: u32 = 9; +const CC_SHORT: u32 = 13; +const CC_CONTEXT_STATE: u32 = 19; +const CC_RING_STOPPED: u32 = 24; +const CC_ABORTED: u32 = 25; +const CC_STOPPED: u32 = 26; + +/// The memory the controller reaches. +pub struct Mem { + regions: Vec<(u64, Vec)>, + pub faults: u32, +} + +impl Mem { + fn at(&self, addr: u64, len: usize) -> Option<(usize, usize)> { + self.regions.iter().enumerate().find_map(|(i, (base, bytes))| { + let off = addr.checked_sub(*base)? as usize; + (off + len <= bytes.len()).then_some((i, off)) + }) + } + + pub fn read(&mut self, addr: u64, out: &mut [u8]) -> bool { + match self.at(addr, out.len()) { + Some((i, off)) => { + out.copy_from_slice(&self.regions[i].1[off..off + out.len()]); + true + } + None => { + self.faults += 1; + false + } + } + } + + pub fn write(&mut self, addr: u64, bytes: &[u8]) -> bool { + match self.at(addr, bytes.len()) { + Some((i, off)) => { + self.regions[i].1[off..off + bytes.len()].copy_from_slice(bytes); + true + } + None => { + self.faults += 1; + false + } + } + } + + fn u32(&mut self, addr: u64) -> u32 { + let mut b = [0; 4]; + self.read(addr, &mut b); + u32::from_le_bytes(b) + } + + fn u64(&mut self, addr: u64) -> u64 { + let mut b = [0; 8]; + self.read(addr, &mut b); + u64::from_le_bytes(b) + } + + fn put32(&mut self, addr: u64, v: u32) { + self.write(addr, &v.to_le_bytes()); + } + + fn trb(&mut self, addr: u64) -> Trb { + Trb { param: self.u64(addr), status: self.u32(addr + 8), control: self.u32(addr + 12) } + } + + /// A region a test lends, as a client lends one to a session. + pub fn lend(&mut self, base: u64, bytes: usize) { + self.regions.push((base, vec![0; bytes])); + } +} + +/// The grant, as the driver reaches it. +#[derive(Clone)] +pub struct Dma { + mem: Rc>, + bytes: usize, + /// Every store and barrier in order: `(offset, is_control_word)` for a + /// store, `None` for a `publish`. + pub log: Rc>>>, +} + +impl Dma { + fn get(&self, at: usize) -> [u8; N] { + let mut b = [0; N]; + assert!(self.mem.borrow_mut().read(GRANT_BASE + at as u64, &mut b), "the driver read past its grant at {at:#x}"); + b + } + + fn set(&self, at: usize, b: &[u8]) { + assert!(self.mem.borrow_mut().write(GRANT_BASE + at as u64, b), "the driver wrote past its grant at {at:#x}"); + self.log.borrow_mut().push(Some(at)); + } +} + +impl DmaBuffers for Dma { + fn bytes(&self) -> usize { + self.bytes + } + fn device_addr(&self, at: usize) -> u64 { + GRANT_BASE + at as u64 + } + fn read16(&self, at: usize) -> u16 { + u16::from_le_bytes(self.get(at)) + } + fn read32(&self, at: usize) -> u32 { + u32::from_le_bytes(self.get(at)) + } + fn read64(&self, at: usize) -> u64 { + u64::from_le_bytes(self.get(at)) + } + fn write16(&self, at: usize, v: u16) { + self.set(at, &v.to_le_bytes()); + } + fn write32(&self, at: usize, v: u32) { + self.set(at, &v.to_le_bytes()); + } + fn write64(&self, at: usize, v: u64) { + self.set(at, &v.to_le_bytes()); + } + fn publish(&self) { + self.log.borrow_mut().push(None); + } + fn observe(&self) {} +} + +/// What a device says to one request or transfer. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Reply { + /// Done: the bytes an IN moves, or none for an OUT taken. + Data(Vec), + Stall, + /// A stall whose Transfer Event is lost: the endpoint reads Halted in its + /// context, and no event says so. + StallUnannounced, + /// Nothing yet: the transfer stays the endpoint's until the device has + /// something, as an interrupt IN waits for a report. + Wait, +} + +/// A device's functions, as the bus reaches them. +pub trait Function { + /// A bus reset reached it: every state returns to Default (USB 2.0 + /// §9.1.1). + fn reset(&mut self) {} + /// A request on EP0: its Setup packet and an OUT data stage's bytes. + fn control(&mut self, setup: [u8; 8], out: &[u8]) -> Reply; + /// A transfer on endpoint `address`: an OUT's bytes, or an IN of at most + /// `max` bytes. + fn transfer(&mut self, address: u8, out: Option<&[u8]>, max: usize) -> Reply; +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum EpState { + Running, + Halted, + Stopped, +} + +struct Ep { + dequeue: u64, + cycle: bool, + state: EpState, + /// The doorbell rang and a TD may be waiting. + rung: bool, + /// A Setup packet whose Data and Status stages are still to come. + setup: Option<[u8; 8]>, +} + +struct Slot { + port: usize, + eps: BTreeMap, +} + +/// How the controller answers commands. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Answers { + Always, + /// Never answers a command; an abort is answered as §4.6.1.2 says. + Never, + /// Never answers a command, nor its abort: a controller that is gone. + NotEvenAnAbort, + /// Answers every endpoint command with this completion code. + Refuses(u32), +} + +pub struct Xhc { + pub mem: Rc>, + pub ports: Vec, + pub devices: Vec>>, + pub usb3: Vec, + pub usbcmd: u32, + crcr: u64, + dcbaap: u64, + erstba: u64, + erdp: u64, + /// The command ring's dequeue and cycle once running. + cmd: Option<(u64, bool)>, + cmd_rung: bool, + /// A command consumed and not answered. + current: Option, + slots: BTreeMap, + /// The event ring: segment base, size, enqueue index, cycle. + event: Option<(u64, u32, u32, bool)>, + pub now: Nanos, + pub answers: Answers, + legsup: u32, + /// Firmware holds HC BIOS Owned and never lets go of it (§7.1.1). + pub firmware_keeps: bool, + /// HCCPARAMS1.AC64: 64-bit addressing (§5.3.6). + pub ac64: bool, + legctl: u32, + /// Every PORTSC write the driver made that broke an invariant. + pub violations: Vec<(usize, invariants::Violation)>, + /// Commands the controller ran: when, and their TRB type. + pub commands: Vec<(Nanos, u32)>, + /// Interrupter 0's Interrupt Pending and Enable (§5.5.2.1), and Event + /// Handler Busy (§5.5.2.3.3). + pending: bool, + enabled: bool, + busy: bool, + /// An interrupt was raised and no record of it has been taken. + raised: bool, +} + +/// The register window, as the driver reaches it. +#[derive(Clone)] +pub struct Regs(pub Rc>); + +impl Xhc { + /// A controller of `ports` root-hub ports, the first `usb2` of them USB2 + /// and the rest USB3, each holding what `devices` says. + pub fn build(ports: Vec, devices: Vec>>, usb2: usize, grant: usize) -> (Regs, Dma, Rc>) { + let mem = Rc::new(RefCell::new(Mem { regions: vec![(GRANT_BASE, vec![0; grant])], faults: 0 })); + let usb3 = (0..ports.len()).map(|p| p >= usb2).collect(); + let xhc = Xhc { + mem: mem.clone(), + ports, + devices, + usb3, + usbcmd: 0, + crcr: 0, + dcbaap: 0, + erstba: 0, + erdp: 0, + cmd: None, + cmd_rung: false, + current: None, + slots: BTreeMap::new(), + event: None, + now: 0, + answers: Answers::Always, + legsup: 1, + firmware_keeps: false, + ac64: true, + legctl: 0, + violations: Vec::new(), + commands: Vec::new(), + pending: false, + enabled: false, + busy: false, + raised: false, + }; + let dma = Dma { mem: mem.clone(), bytes: grant, log: Rc::default() }; + (Regs(Rc::new(RefCell::new(xhc))), dma, mem) + } + + fn read(&self, at: usize) -> u32 { + let usb2 = self.usb3.iter().filter(|u| !**u).count() as u32; + let n = self.ports.len() as u32; + match at { + 0x00 => 0x0120_0000 | CAPLENGTH as u32, + 0x04 => SLOTS as u32 | (1 << 8) | (n << 24), + 0x08 | 0x0C => 0, + // AC64, 32-byte contexts, the extended capabilities at XECP. + 0x10 => u32::from(self.ac64) | ((XECP as u32 / 4) << 16), + 0x14 => DB as u32, + 0x18 => RTS as u32, + a if a == CAPLENGTH => self.usbcmd, + a if a == CAPLENGTH + 4 => u32::from(self.usbcmd & USBCMD_RS == 0) * USBSTS_HCH, + a if a == CAPLENGTH + 8 => 1, + a if a == CAPLENGTH + 0x18 => u32::from(self.cmd.is_some()) << 3, + a if (CAPLENGTH + 0x400..CAPLENGTH + 0x400 + 0x10 * self.ports.len()).contains(&a) => { + let p = (a - CAPLENGTH - 0x400) / 0x10; + if (a - CAPLENGTH - 0x400).is_multiple_of(0x10) { self.ports[p].raw() } else { 0 } + } + // Legacy Support, then a Supported Protocol capability for each + // protocol some port speaks (§7.2), each four dwords on. + a if a == XECP => { + (self.legsup | u32::from(self.firmware_keeps) << 16) & !(0xFF << 8) | (u32::from(usb2 > 0 || n > usb2) * 4) << 8 + } + a if a == XECP + 4 => self.legctl, + a if a == XECP + 0x10 && usb2 > 0 => 2 | (u32::from(n > usb2) * 4) << 8 | (2 << 24), + a if a == XECP + 0x18 && usb2 > 0 => 1 | (usb2 << 8), + a if a == XECP + 0x10 || a == XECP + 0x20 => 2 | (3 << 24), + a if a == XECP + 0x18 || a == XECP + 0x28 => (usb2 + 1) | ((n - usb2) << 8), + a if a == XECP + 0x14 || a == XECP + 0x24 => u32::from_le_bytes(*b"USB "), + _ => 0, + } + } + + fn write(&mut self, at: usize, v: u32) { + match at { + a if a == CAPLENGTH => { + if v & USBCMD_HCRST != 0 { + // §5.4.1: a reset puts every register at its default and + // the controller forgets every ring and slot. + self.usbcmd = 0; + self.cmd = None; + self.current = None; + self.slots.clear(); + self.event = None; + return; + } + self.usbcmd = v; + } + a if a == CAPLENGTH + 0x18 => { + if v & CRCR_CA != 0 { + return self.abort(); + } + if self.cmd.is_none() { + self.crcr = (self.crcr & !0xFFFF_FFFF) | u64::from(v); + } + } + a if a == CAPLENGTH + 0x1C => { + if self.cmd.is_none() { + self.crcr = (self.crcr & 0xFFFF_FFFF) | u64::from(v) << 32; + } + } + a if a == CAPLENGTH + 0x30 => self.dcbaap = (self.dcbaap & !0xFFFF_FFFF) | u64::from(v), + a if a == CAPLENGTH + 0x34 => self.dcbaap = (self.dcbaap & 0xFFFF_FFFF) | u64::from(v) << 32, + a if (CAPLENGTH + 0x400..CAPLENGTH + 0x400 + 0x10 * self.ports.len()).contains(&a) => { + let p = (a - CAPLENGTH - 0x400) / 0x10; + if (a - CAPLENGTH - 0x400).is_multiple_of(0x10) { + if let Some(why) = invariants::check_write(crate::portsc::Write::of(v), self.ports[p].read()) { + self.violations.push((p, why)); + } + let now = self.now; + self.ports[p].write(v, now); + // PR or WPR: the device sees a bus reset. + if v & (1 << 4 | 1 << 31) != 0 + && let Some(device) = self.devices.get_mut(p).and_then(|d| d.as_mut()) + { + device.reset(); + } + } + } + // IMAN: Interrupt Pending is write-1-to-clear, Interrupt Enable + // read-write. + a if a == RTS + 0x20 => { + if v & 1 != 0 { + self.pending = false; + } + self.enabled = v & 2 != 0; + self.assert(); + } + a if a == RTS + 0x30 => { + self.erstba = (self.erstba & !0xFFFF_FFFF) | u64::from(v); + } + a if a == RTS + 0x34 => { + self.erstba = (self.erstba & 0xFFFF_FFFF) | u64::from(v) << 32; + let mut mem = self.mem.borrow_mut(); + let (base, size) = (mem.u64(self.erstba), mem.u32(self.erstba + 8)); + self.event = Some((base, size, 0, true)); + } + a if a == RTS + 0x38 => { + self.erdp = (self.erdp & !0xFFFF_FFFF) | u64::from(v & !0xF); + // EHB is write-1-to-clear; cleared with an interrupt still + // pending, that interrupt is raised now. + if v & (1 << 3) != 0 { + self.busy = false; + self.assert(); + } + } + a if a == RTS + 0x3C => self.erdp = (self.erdp & 0xFFFF_FFFF) | u64::from(v) << 32, + a if a == DB => { + if self.cmd.is_none() { + self.cmd = Some((self.crcr & !0x3F, self.crcr & 1 == 1)); + } + self.cmd_rung = true; + } + a if (DB + 4..DB + 4 * (SLOTS + 1)).contains(&a) => { + let slot = ((a - DB) / 4) as u8; + if let Some(ep) = self.slots.get_mut(&slot).and_then(|s| s.eps.get_mut(&(v as u8))) { + ep.rung = true; + if ep.state == EpState::Stopped { + ep.state = EpState::Running; + } + } + } + a if a == XECP => self.legsup = v & !(1 << 16) | u32::from(self.firmware_keeps) << 16, + a if a == XECP + 4 => self.legctl = v, + _ => {} + } + } + + /// §4.6.1.2: the command running ends Command Aborted, the ring stops, + /// and Command Ring Stopped says where. + fn abort(&mut self) { + if self.answers == Answers::NotEvenAnAbort { + return; + } + if let Some(at) = self.current.take() { + self.command_event(at, CC_ABORTED, 0); + } + let at = self.cmd.map_or(0, |(at, _)| at); + self.command_event(at, CC_RING_STOPPED, 0); + self.cmd_rung = false; + } + + fn post(&mut self, trb: Trb) { + let Some((base, size, enq, cycle)) = self.event else { return }; + let at = base + u64::from(enq) * 16; + let mut mem = self.mem.borrow_mut(); + mem.write(at, &trb.param.to_le_bytes()); + mem.put32(at + 8, trb.status); + mem.put32(at + 12, (trb.control & !1) | u32::from(cycle)); + drop(mem); + let next = enq + 1; + self.event = Some(if next == size { (base, size, 0, !cycle) } else { (base, size, next, cycle) }); + self.pending = true; + self.assert(); + } + + /// §4.17.2, with no moderation: an interrupt is raised while one is + /// pending, the interrupter is enabled and no handler is busy, and + /// raising it makes the handler busy. + fn assert(&mut self) { + if self.pending && self.enabled && !self.busy { + self.busy = true; + self.raised = true; + } + } + + /// An interrupt raised for an event the handler has already taken: what a + /// controller's assertion that lands inside a drain leaves behind. + pub fn raise_for_nothing(&mut self) { + self.pending = true; + self.busy = true; + self.raised = true; + } + + /// Whether an interrupt was raised since the last call, taking it: the + /// claim's interrupt record, as usbd reads it. + pub fn take_interrupt(&mut self) -> bool { + core::mem::take(&mut self.raised) + } + + fn command_event(&mut self, at: u64, code: u32, slot: u8) { + self.post(Trb { param: at, status: code << 24, control: (33 << 10) | u32::from(slot) << 24 }); + } + + fn transfer_event(&mut self, at: u64, code: u32, residue: u32, slot: u8, dci: u8) { + self.post(Trb { param: at, status: code << 24 | residue, control: (32 << 10) | u32::from(dci) << 16 | u32::from(slot) << 24 }); + } + + /// The TRB at `at` of a ring whose consumer cycle is `cycle`, following + /// Links (§4.11.5.1): `None` where the producer has not put one. + fn next_trb(&self, at: &mut u64, cycle: &mut bool) -> Option<(u64, Trb)> { + for _ in 0..2 { + let trb = self.mem.borrow_mut().trb(*at); + if (trb.control & 1 == 1) != *cycle { + return None; + } + if (trb.control >> 10) & 0x3F == 6 { + *at = trb.param & !0xF; + if trb.control & 2 != 0 { + *cycle = !*cycle; + } + continue; + } + let here = *at; + *at += 16; + return Some((here, trb)); + } + None + } + + fn output_context(&self, slot: u8) -> u64 { + let mut mem = self.mem.borrow_mut(); + mem.u64(self.dcbaap + u64::from(slot) * 8) + } + + fn set_state(&mut self, slot: u8, dci: u8, state: EpState) { + if let Some(ep) = self.slots.get_mut(&slot).and_then(|s| s.eps.get_mut(&dci)) { + ep.state = state; + } + let out = self.output_context(slot); + let code = match state { + EpState::Running => 1, + EpState::Halted => 2, + EpState::Stopped => 3, + }; + if out != 0 { + self.mem.borrow_mut().put32(out + u64::from(dci) * 32, code); + } + } + + /// Let time pass to `now`: ports move, then the command ring and every + /// rung endpoint run what they hold. Answers whether anything happened. + pub fn run(&mut self, now: Nanos) -> bool { + self.now = now; + let mut moved = false; + for p in 0..self.ports.len() { + self.ports[p].tick(now); + if self.ports[p].take_event() { + self.post(Trb { param: ((p + 1) as u64) << 24, status: CC_SUCCESS << 24, control: 34 << 10 }); + moved = true; + } + } + if self.usbcmd & USBCMD_RS == 0 { + return moved; + } + while self.cmd_rung && self.current.is_none() { + let Some((mut at, mut cycle)) = self.cmd else { break }; + let Some((here, trb)) = self.next_trb(&mut at, &mut cycle) else { + self.cmd_rung = false; + break; + }; + self.cmd = Some((at, cycle)); + moved = true; + self.commands.push((now, (trb.control >> 10) & 0x3F)); + match self.answers { + Answers::Never | Answers::NotEvenAnAbort => self.current = Some(here), + _ => self.command(here, trb), + } + } + let rung: Vec<(u8, u8)> = self + .slots + .iter() + .flat_map(|(s, slot)| slot.eps.iter().filter(|(_, e)| e.rung && e.state == EpState::Running).map(|(d, _)| (*s, *d))) + .collect(); + for (slot, dci) in rung { + while self.td(slot, dci) { + moved = true; + } + } + moved + } + + fn command(&mut self, at: u64, trb: Trb) { + let slot = (trb.control >> 24) as u8; + let dci = ((trb.control >> 16) & 0x1F) as u8; + let kind = (trb.control >> 10) & 0x3F; + if let (Answers::Refuses(code), 14..=16) = (self.answers, kind) { + return self.command_event(at, code, slot); + } + let code = match kind { + 23 => CC_SUCCESS, + 9 => match (1..=SLOTS as u8).find(|s| !self.slots.contains_key(s)) { + Some(s) => { + self.slots.insert(s, Slot { port: usize::MAX, eps: BTreeMap::new() }); + return self.command_event(at, CC_SUCCESS, s); + } + None => CC_SLOTS, + }, + 10 => { + self.slots.remove(&slot); + CC_SUCCESS + } + 11 => { + let input = trb.param & !0xF; + let mut mem = self.mem.borrow_mut(); + let port = ((mem.u32(input + 32 + 4) >> 16) & 0xFF) as usize; + let dequeue = mem.u64(input + 64 + 8); + drop(mem); + match self.slots.get_mut(&slot) { + Some(s) if port >= 1 && port <= self.ports.len() => { + s.port = port - 1; + s.eps.insert(1, Ep { dequeue: dequeue & !0xF, cycle: dequeue & 1 == 1, state: EpState::Running, rung: false, setup: None }); + self.set_state(slot, 1, EpState::Running); + CC_SUCCESS + } + _ => 5, + } + } + 12 => { + let input = trb.param & !0xF; + let mut mem = self.mem.borrow_mut(); + let (drop_flags, add) = (mem.u32(input), mem.u32(input + 4)); + let mut added = Vec::new(); + for d in 2..32u8 { + if add & (1 << d) != 0 { + let dequeue = mem.u64(input + (u64::from(d) + 1) * 32 + 8); + added.push((d, dequeue)); + } + } + drop(mem); + let Some(s) = self.slots.get_mut(&slot) else { return self.command_event(at, 11, slot) }; + for d in 2..32u8 { + if drop_flags & (1 << d) != 0 { + s.eps.remove(&d); + } + } + for &(d, dequeue) in &added { + s.eps.insert(d, Ep { dequeue: dequeue & !0xF, cycle: dequeue & 1 == 1, state: EpState::Running, rung: false, setup: None }); + } + for (d, _) in added { + self.set_state(slot, d, EpState::Running); + } + CC_SUCCESS + } + 13 => CC_SUCCESS, + 14 => match self.state(slot, dci) { + Some(EpState::Halted) => { + self.set_state(slot, dci, EpState::Stopped); + CC_SUCCESS + } + _ => CC_CONTEXT_STATE, + }, + 15 => match self.state(slot, dci) { + Some(EpState::Running) => { + // §4.6.9: a TD the endpoint was on is answered Stopped. + let waiting = self.slots.get(&slot).and_then(|s| s.eps.get(&dci)).filter(|e| e.rung).map(|e| (e.dequeue, e.cycle)); + if let Some((mut deq, mut cycle)) = waiting + && let Some((here, _)) = self.next_trb(&mut deq, &mut cycle) { + self.transfer_event(here, CC_STOPPED, 0, slot, dci); + } + self.set_state(slot, dci, EpState::Stopped); + if let Some(ep) = self.slots.get_mut(&slot).and_then(|s| s.eps.get_mut(&dci)) { + ep.rung = false; + ep.setup = None; + } + CC_SUCCESS + } + _ => CC_CONTEXT_STATE, + }, + 16 => match self.slots.get_mut(&slot).and_then(|s| s.eps.get_mut(&dci)) { + Some(ep) if ep.state == EpState::Stopped => { + ep.dequeue = trb.param & !0xF; + ep.cycle = trb.param & 1 == 1; + ep.setup = None; + CC_SUCCESS + } + _ => CC_CONTEXT_STATE, + }, + _ => 5, + }; + self.command_event(at, code, slot); + } + + fn state(&self, slot: u8, dci: u8) -> Option { + self.slots.get(&slot)?.eps.get(&dci).map(|e| e.state) + } + + /// Run the next TD of (`slot`, `dci`), if the producer has put one and + /// the device answers it; `false` when it is not run now. + fn td(&mut self, slot: u8, dci: u8) -> bool { + let (port, mut at, mut cycle) = { + let s = &self.slots[&slot]; + let ep = &s.eps[&dci]; + (s.port, ep.dequeue, ep.cycle) + }; + // The TD: TRBs up to one with no Chain bit, or a control transfer's + // stages up to its Status. + let mut trbs = Vec::new(); + loop { + let before = at; + let Some((here, trb)) = self.next_trb(&mut at, &mut cycle) else { + if trbs.is_empty() { + self.slots.get_mut(&slot).unwrap().eps.get_mut(&dci).unwrap().rung = false; + } + return false; + }; + // The driver's contract (`Ring::enqueue`): a TD starts behind any + // Link it would cross, so none spans the ring's end (§4.11.7.1). + assert!(trbs.is_empty() || here == before, "slot {slot} dci {dci}: a TD crosses the ring's Link at {before:#x}"); + let kind = (trb.control >> 10) & 0x3F; + trbs.push((here, trb)); + let ends = match kind { + 2 | 3 => false, + 4 => true, + _ => trb.control & (1 << 4) == 0, + }; + if ends { + break; + } + } + let Some(device) = self.devices.get_mut(port).and_then(|d| d.as_mut()) else { return false }; + if !self.ports[port].present() { + return false; + } + let control = dci == 1; + let address = (dci / 2) | if dci % 2 == 1 { 0x80 } else { 0 }; + let data: Vec<(u64, Trb)> = trbs.iter().copied().filter(|(_, t)| matches!((t.control >> 10) & 0x3F, 1 | 3)).collect(); + let total: usize = data.iter().map(|(_, t)| (t.status & 0x1_FFFF) as usize).sum(); + let inbound = if control { data.first().is_some_and(|(_, t)| t.control & (1 << 16) != 0) } else { dci % 2 == 1 }; + let mut out = Vec::new(); + if !inbound { + let mut mem = self.mem.borrow_mut(); + for (_, t) in &data { + let mut b = vec![0; (t.status & 0x1_FFFF) as usize]; + mem.read(t.param, &mut b); + out.extend(b); + } + } + let reply = if control { + let setup = trbs.first().map(|(_, t)| t.param.to_le_bytes()).expect("a Setup stage"); + device.control(setup, &out) + } else { + device.transfer(address, (!inbound).then_some(&out[..]), total) + }; + let last = trbs.last().copied().expect("a TD"); + match reply { + Reply::Wait => return false, + Reply::Stall => { + let on = data.first().copied().unwrap_or(last).0; + self.transfer_event(on, CC_STALL, 0, slot, dci); + self.set_state(slot, dci, EpState::Halted); + } + Reply::StallUnannounced => self.set_state(slot, dci, EpState::Halted), + Reply::Data(bytes) => { + let bytes = if inbound { bytes } else { Vec::new() }; + let mut moved = 0usize; + let mut short = None; + for &(here, t) in &data { + let len = (t.status & 0x1_FFFF) as usize; + let take = len.min(bytes.len().saturating_sub(moved)); + if inbound { + self.mem.borrow_mut().write(t.param, &bytes[moved..moved + take]); + } + moved += take; + if inbound && take < len { + short = Some((here, (len - take) as u32, t.control & (1 << 2) != 0)); + break; + } + } + match short { + // §4.10.1.1: the TD ends at a short packet, with an event + // only where ISP asks for one — but a control transfer's + // Status stage still runs. + Some((here, residue, isp)) => { + if isp { + self.transfer_event(here, CC_SHORT, residue, slot, dci); + } + if control { + self.transfer_event(last.0, CC_SUCCESS, 0, slot, dci); + } + } + None => self.transfer_event(last.0, CC_SUCCESS, 0, slot, dci), + } + } + } + let ep = self.slots.get_mut(&slot).unwrap().eps.get_mut(&dci).unwrap(); + ep.dequeue = at; + ep.cycle = cycle; + true + } + + /// When a port next moves on its own clock. + pub fn due(&self) -> Option { + self.ports.iter().filter_map(FakePort::due).min() + } +} + +impl Registers for Regs { + fn bytes(&self) -> usize { + BAR_BYTES + } + fn read8(&self, at: usize) -> u8 { + (self.0.borrow().read(at & !3) >> ((at & 3) * 8)) as u8 + } + fn read16(&self, at: usize) -> u16 { + (self.0.borrow().read(at & !3) >> ((at & 3) * 8)) as u16 + } + fn read32(&self, at: usize) -> u32 { + self.0.borrow().read(at) + } + fn write8(&self, _: usize, _: u8) { + panic!("no xHCI register is written a byte at a time"); + } + fn write16(&self, _: usize, _: u16) { + panic!("no xHCI register is written sixteen bits at a time"); + } + fn write32(&self, at: usize, v: u32) { + self.0.borrow_mut().write(at, v); + } +} diff --git a/lib/xhci/sim/src/hub.rs b/system/usbd/xhci/src/stub/port.rs similarity index 91% rename from lib/xhci/sim/src/hub.rs rename to system/usbd/xhci/src/stub/port.rs index d08604eee73..b0e6494eab4 100644 --- a/lib/xhci/sim/src/hub.rs +++ b/system/usbd/xhci/src/stub/port.rs @@ -10,8 +10,8 @@ //! flag goes from 0 to 1, so a driver that looks only when told misses what //! no edge reports. -use toyos_xhci::port::Nanos; -use toyos_xhci::Portsc; +use crate::exec::Nanos; +use crate::portsc::Portsc; const CCS: u32 = 1 << 0; const PED: u32 = 1 << 1; @@ -117,7 +117,30 @@ impl FakePort { /// port to Enabled with the link at U0 and no reset from anybody, which is /// §4.19.1.2's own sequence and the thing a USB2-shaped driver resets away. pub fn superspeed(behaviour: ResetBehaviour) -> Self { - Self { superspeed: true, ..Self::empty(behaviour) } + Self { superspeed: true, speed: 4, ..Self::empty(behaviour) } + } + + /// A SuperSpeed port whose device is detected and whose link has not + /// trained: Polling, the port disabled, its connect edge raised. + pub fn untrained(behaviour: ResetBehaviour) -> Self { + let mut port = Self::superspeed(behaviour); + port.present = true; + port.set(PP | CCS | CSC | (7 << PLS_SHIFT)); + port + } + + /// When a reset in flight finishes, for a clock that jumps to it. + pub fn due(&self) -> Option { + let since = self.resetting_since?; + match self.behaviour { + ResetBehaviour::Completes { after } => Some(since + after), + ResetBehaviour::Never => None, + _ => Some(since + 1_000_000), + } + } + + pub fn present(&self) -> bool { + self.present } pub fn attach(&mut self) { diff --git a/system/usbd/xhci/src/tests.rs b/system/usbd/xhci/src/tests.rs new file mode 100644 index 00000000000..f7f8c3d4f46 --- /dev/null +++ b/system/usbd/xhci/src/tests.rs @@ -0,0 +1,899 @@ +//! The driver against the stub xHC, seeded where the order of events is the +//! question. + +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::rc::Rc; + +use toyos_input::wire::Record; + +use crate::exec::{Dead, Host, Nanos, Tasks}; +use crate::hc::{self, Reset}; +use crate::input::{Opened, Owner, Sent}; +use crate::msc::{Event, Work}; +use crate::stub::devices::{gpt_disk, Keyboard, Pipes, Stick}; +use crate::stub::port::{FakePort, ResetBehaviour}; +use crate::stub::{Answers, Dma, Function, Regs, Xhc, LENT_BASE}; +use crate::usb::{Config, Usb}; + +const MS: Nanos = 1_000_000; +const QUICK: ResetBehaviour = ResetBehaviour::Completes { after: MS }; +const GRANT: usize = 2 * 1024 * 1024; +/// The partition every test stick carries. +const UNIQUE: [u8; 16] = [0x33; 16]; + +#[derive(Clone)] +struct TestHost { + now: Rc>, + lines: Rc>>, +} + +impl Host for TestHost { + fn now(&self) -> Nanos { + self.now.get() + } + fn say(&self, line: core::fmt::Arguments<'_>) { + let line = line.to_string(); + if std::env::var_os("USBD_SAY").is_some() { + eprintln!("[{:>9} us] {line}", self.now.get() / 1000); + } + self.lines.borrow_mut().push(line); + } +} + +/// The span of [`UNIQUE`] the table names, which is what a test's reader +/// reads of a disk. +type Table = Option<(u64, u64)>; + +fn table(sectors: &mut dyn toyos_gpt::Sectors) -> Table { + let located = toyos_gpt::locate(sectors, toyos_gpt::Guid(UNIQUE)).ok()?; + let p = located.partition(); + Some((p.first_lba(), p.lba_count().get())) +} + +/// The owner of input, as a test plays it. +#[derive(Default)] +struct Mock { + connected: bool, + /// Records it takes before it is full; `None` is no bound. + room: Option, + /// Whether it accepts a connection. + refuses: bool, + opens: u32, + got: Vec, + /// Records per connection. + connections: Vec>, +} + +impl Owner for Mock { + fn open(&mut self) -> Opened { + self.opens += 1; + if self.refuses { + return Opened::Refused; + } + self.connected = true; + self.connections.push(Vec::new()); + Opened::Opened + } + fn send(&mut self, record: &Record) -> Sent { + if !self.connected { + return Sent::Ended; + } + if self.room == Some(0) { + return Sent::Full; + } + if let Some(room) = &mut self.room { + *room -= 1; + } + self.got.push(*record); + self.connections.last_mut().unwrap().push(*record); + Sent::Taken + } + fn close(&mut self) { + self.connected = false; + } +} + +struct World { + regs: Regs, + dma: Dma, + host: TestHost, +} + +type Driver = Usb; + +/// Ends a test's driver with what it holds: declared after its tasks, it +/// drops first. +struct End<'a>(&'a Driver); + +impl Drop for End<'_> { + fn drop(&mut self) { + self.0.hc.ending.set(true); + } +} + +impl World { + fn new(ports: Vec<(FakePort, Option>)>, usb2: usize) -> Self { + let (ports, devices) = ports.into_iter().unzip(); + let (regs, dma, _) = Xhc::build(ports, devices, usb2, GRANT); + let host = TestHost { now: Rc::new(Cell::new(0)), lines: Rc::default() }; + Self { regs, dma, host } + } + + fn xhc(&self) -> std::cell::RefMut<'_, Xhc> { + self.regs.0.borrow_mut() + } + + fn start(&self, config: Config) -> Driver { + let reset = Reset::new(self.regs.clone(), &self.host).expect("the stub resets"); + assert!(reset.grant_bytes() <= GRANT, "{} bytes", reset.grant_bytes()); + let hc = reset.start(self.dma.clone(), &self.host).expect("the stub runs"); + Usb::new(hc, self.host.clone(), config, table) + } + + fn said(&self, what: &str) -> bool { + self.host.lines.borrow().iter().any(|l| l.contains(what)) + } + + /// Run the driver and the stub together until the clock reaches `until`. + fn run(&self, usb: &Driver, tasks: &mut Tasks<'_>, owner: &mut Mock, until: Nanos) -> Result<(), Dead> { + loop { + let now = self.host.now.get(); + let mut quiet = 0; + let mut at = None; + let mut turns = 0; + // The stub and the driver in turn at this instant, until neither + // has anything to do. + while quiet < 2 { + turns += 1; + assert!(turns < 100_000, "the driver and the stub never went quiet at {now} ns"); + let moved = self.xhc().run(now); + // As usbd's binary does: the event ring is read only when an + // interrupt record says to. + let raised = self.xhc().take_interrupt(); + let mut took = false; + if raised { + while usb.hc.take_events() && usb.hc.event_waiting() {} + took = true; + } + at = tasks.run(&usb.wake)?; + usb.input.borrow_mut().flush(owner, now); + quiet = if moved || took { 0 } else { quiet + 1 }; + } + if now >= until { + return Ok(()); + } + let due = self.xhc().due().filter(|d| *d > now); + let next = [at, due, Some(until)].into_iter().flatten().min().unwrap().max(now + 1); + self.host.now.set(next.min(until)); + } + } +} + +/// What a test pushes a keyboard's reports into. +type Reports = Rc>>>; +/// What a test reads a stick's sectors from, and sets its pipes by. +type Sectors = Rc>>; +/// One piece of work's token and whether it was done. +type Answer = ((u64, u32), bool); + +fn keyboard() -> (Box, Reports) { + let (kb, reports) = Keyboard::new(); + (Box::new(kb), reports) +} + +fn stick(serial: &str) -> (Stick, Sectors, Rc>) { + let stick = Stick::new(gpt_disk(16384, UNIQUE, 2048, 8192), serial); + let (disk, pipes) = (stick.disk.clone(), stick.pipes.clone()); + (stick, disk, pipes) +} + +const BOTH: Config = Config { input: true, disks: true }; + +fn keys(got: &[Record]) -> Vec<(u8, bool)> { + got.iter() + .filter_map(|r| match r { + Record::Key { key, .. } => Some((key.keycode, key.pressed())), + _ => None, + }) + .collect() +} + +/// A keyboard present at bring-up is reset, enumerated, bound by its report +/// descriptor in report protocol, and its reports reach the owner as key +/// transitions. +#[test] +fn a_keyboard_present_at_bring_up_delivers_its_reports() { + let (kb, reports) = keyboard(); + let w = World::new(vec![(FakePort::occupied(QUICK), Some(kb)), (FakePort::empty(QUICK), None)], 2); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 500 * MS).unwrap(); + assert!(usb.settled()); + assert!(w.said("collection 0: a Keyboard"), "{:#?}", w.host.lines.borrow()); + reports.borrow_mut().push_back(vec![0x02, 0, 0x04, 0, 0, 0, 0, 0]); + w.run(&usb, &mut tasks, &mut owner, 510 * MS).unwrap(); + reports.borrow_mut().push_back(vec![0; 8]); + w.run(&usb, &mut tasks, &mut owner, 520 * MS).unwrap(); + assert_eq!(keys(&owner.got), [(0xE1, true), (0x04, true), (0xE1, false), (0x04, false)]); + assert_eq!(owner.opens, 1); + assert_eq!(w.xhc().violations, []); + assert_eq!(w.xhc().mem.borrow().faults, 0); +} + +/// A device present at bring-up and one plugged in later run the same path: +/// the controller is asked the same commands in the same order. +#[test] +fn a_device_plugged_in_later_runs_the_same_path_as_one_present_at_bring_up() { + let trace = |present: bool| { + let (kb, _) = keyboard(); + let port = if present { FakePort::occupied(QUICK) } else { FakePort::empty(QUICK) }; + let w = World::new(vec![(port, Some(kb))], 1); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 300 * MS).unwrap(); + if !present { + w.xhc().ports[0].attach(); + } + w.run(&usb, &mut tasks, &mut owner, 900 * MS).unwrap(); + assert!(usb.named.borrow().contains_key(&0), "present: {present}"); + let commands: Vec = w.xhc().commands.iter().map(|(_, c)| *c).collect(); + commands + }; + assert_eq!(trace(true), trace(false)); +} + +/// A USB 2 port is debounced 100 ms (USB 2.0 §7.1.7.3) and given 10 ms of +/// reset recovery before its device is addressed; a USB 3 port trains its +/// own link and its device is addressed at once. +#[test] +fn a_usb2_port_waits_its_debounce_and_reset_recovery_and_a_usb3_port_waits_neither() { + let addressed = |port: FakePort, usb2: usize| { + let (kb, _) = keyboard(); + let w = World::new(vec![(port, Some(kb))], usb2); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 50 * MS).unwrap(); + w.xhc().ports[0].attach(); + let attached = w.host.now.get(); + w.run(&usb, &mut tasks, &mut owner, 800 * MS).unwrap(); + let at = w.xhc().commands.iter().find(|(_, c)| *c == 11).map(|(at, _)| *at).expect("Address Device"); + at - attached + }; + let usb2 = addressed(FakePort::empty(QUICK), 1); + assert!((100 * MS + MS + 10 * MS..=100 * MS + MS + 12 * MS).contains(&usb2), "{} us", usb2 / 1000); + let usb3 = addressed(FakePort::superspeed(QUICK), 0); + assert!((100 * MS..100 * MS + MS).contains(&usb3), "{} us", usb3 / 1000); +} + +/// The owed test: a stick whose bulk endpoint never answers leaves a +/// keyboard on the same controller delivering every report. Nothing here +/// reads the clock: every report is counted. +#[test] +fn a_stick_whose_bulk_endpoint_never_answers_leaves_a_keyboard_on_the_same_controller_delivering_every_report() { + let (kb, reports) = keyboard(); + let (stick, _, pipes) = stick("SILENT1"); + pipes.set(Pipes::SilentIn); + let w = World::new(vec![(FakePort::occupied(QUICK), Some(Box::new(stick))), (FakePort::occupied(QUICK), Some(kb))], 2); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 400 * MS).unwrap(); + assert!(w.said("collection 0: a Keyboard")); + // Across a minute: past the stick's every deadline, through each rung of + // its ladder and its enumerations again. + let mut want = Vec::new(); + for n in 0..40u8 { + let key = 0x04 + (n % 20); + reports.borrow_mut().push_back(vec![0, 0, key, 0, 0, 0, 0, 0]); + reports.borrow_mut().push_back(vec![0; 8]); + let now = w.host.now.get(); + w.run(&usb, &mut tasks, &mut owner, now + 1_500 * MS).unwrap(); + want.extend([(key, true), (key, false)]); + assert_eq!(keys(&owner.got), want, "report {n}"); + } + assert!(reports.borrow().is_empty()); + assert!(w.said("Reset Recovery") || w.said("the ladder climbs"), "{:#?}", w.host.lines.borrow()); + assert!(w.said("the ladder climbs to its port reset"), "{:#?}", w.host.lines.borrow()); +} + +/// A bulk endpoint that halted with its event lost, and whose device stalls +/// CLEAR_FEATURE(HALT), costs only its stick: the transfer the ring was moved +/// past is taken back rather than dropped, and the keyboard beside it goes on. +#[test] +fn a_halt_whose_clear_feature_stalls_costs_only_its_stick() { + let (kb, reports) = keyboard(); + let (stick, _, pipes) = stick("HALTED1"); + pipes.set(Pipes::HaltUnannounced); + let w = World::new(vec![(FakePort::occupied(QUICK), Some(Box::new(stick))), (FakePort::occupied(QUICK), Some(kb))], 2); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 400 * MS).unwrap(); + assert!(w.said("collection 0: a Keyboard")); + let mut want = Vec::new(); + for n in 0..20u8 { + let key = 0x04 + n; + reports.borrow_mut().push_back(vec![0, 0, key, 0, 0, 0, 0, 0]); + reports.borrow_mut().push_back(vec![0; 8]); + let now = w.host.now.get(); + w.run(&usb, &mut tasks, &mut owner, now + 1_500 * MS).unwrap(); + want.extend([(key, true), (key, false)]); + assert_eq!(keys(&owner.got), want, "report {n}"); + } + assert!(w.said("its device is reset (Offline)"), "{:#?}", w.host.lines.borrow()); +} + +/// The stop before a machine reset: every connected port reset while the +/// controller still runs, then the controller halted, reset and its ports +/// unpowered, with a stick's command still open, which is let go of and not +/// a panic. +#[test] +fn the_stop_resets_every_connected_port_then_halts_resets_and_unpowers_the_controller() { + let (kb, _) = keyboard(); + let (stick, _, pipes) = stick("STOP1"); + let w = World::new( + vec![ + (FakePort::occupied(QUICK), Some(Box::new(stick))), + (FakePort::occupied(QUICK), Some(kb)), + (FakePort::empty(QUICK), None), + ], + 3, + ); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 400 * MS).unwrap(); + assert!(w.said("collection 0: a Keyboard"), "{:#?}", w.host.lines.borrow()); + // The stick's next command goes unanswered, so the stop finds it open. + pipes.set(Pipes::SilentIn); + usb.disks.submit(0, Work::Io { write: false, block: 0, blocks: 1, at: LENT_BASE, token: (0, 1) }); + let now = w.host.now.get(); + w.run(&usb, &mut tasks, &mut owner, now + 50 * MS).unwrap(); + + let stopped = Rc::new(Cell::new(None)); + let mut stop = Tasks::default(); + { + let (usb, stopped) = (&usb, stopped.clone()); + stop.spawn(async move { + stopped.set(Some(usb.hc.stop(&usb.wake).await)); + Ok(()) + }); + } + // As usbd does: the driver ends, so what its tasks hold may be let go. + usb.hc.ending.set(true); + drop(tasks); + let now = w.host.now.get(); + w.run(&usb, &mut stop, &mut owner, now + hc::STOP_NS).unwrap(); + assert_eq!( + stopped.get(), + Some(hc::Stopped { connected: 2, reset: 2, halted: true, cleared: true, ports: 3, unpowered: 3 }) + ); + for port in 0..3 { + assert!(!usb.hc.portsc(port).in_reset(), "port {port} is still in reset"); + } +} + +/// A stick whose connect is only seen after the driver started, as the T14's +/// is once its ports are powered again: the bus is not settled until the +/// stick is read and offered, and an empty port settles once its attach +/// window (USB 2's TSIGATT) has passed with nothing on it. +#[test] +fn a_stick_that_connects_after_the_start_is_offered_before_the_bus_settles() { + let (stick, _, _) = stick("LATE1"); + let w = World::new(vec![(FakePort::empty(QUICK), Some(Box::new(stick))), (FakePort::empty(QUICK), None)], 2); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 20 * MS).unwrap(); + assert!(!usb.settled(), "the bus settled before its ports' attach window was over"); + w.xhc().ports[0].attach(); + let mut offered = false; + for step in 1..=80 { + w.run(&usb, &mut tasks, &mut owner, 20 * MS + step * 10 * MS).unwrap(); + offered |= usb.disks.take_events().iter().any(|e| matches!(e, Event::Arrived { .. })); + if usb.settled() { + assert!(offered, "the bus settled at {} ms with the stick not yet offered", 20 + step * 10); + return; + } + } + panic!("the bus never settled: {:#?}", w.host.lines.borrow()); +} + +/// A bus with nothing on it settles once the attach window has passed, and +/// not before. +#[test] +fn an_empty_bus_settles_when_its_attach_window_ends() { + let w = World::new(vec![(FakePort::empty(QUICK), None)], 1); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 90 * MS).unwrap(); + assert!(!usb.settled()); + w.run(&usb, &mut tasks, &mut owner, 110 * MS).unwrap(); + assert!(usb.settled()); +} + +/// Firmware that never clears HC BIOS Owned: the controller is refused by +/// name after the handoff's bound and left as found, its SMIs and its run +/// state untouched (§4.22.1). +#[test] +fn a_controller_firmware_keeps_is_refused_and_left_as_found() { + struct Ticking(Cell, RefCell>); + impl Host for &Ticking { + fn now(&self) -> Nanos { + self.0.set(self.0.get() + MS); + self.0.get() + } + fn say(&self, line: core::fmt::Arguments<'_>) { + self.1.borrow_mut().push(line.to_string()); + } + } + let w = World::new(vec![(FakePort::empty(QUICK), None)], 1); + w.xhc().firmware_keeps = true; + // Running, as firmware left it. + w.xhc().usbcmd = 1; + let host = Ticking(Cell::new(0), RefCell::new(Vec::new())); + match Reset::new(w.regs.clone(), &&host) { + Err(hc::Refusal::FirmwareKeepsIt(legsup)) => assert_ne!(legsup & 1 << 16, 0), + Err(other) => panic!("refused for {other}"), + Ok(_) => panic!("a controller firmware keeps was taken"), + } + assert_eq!(w.xhc().usbcmd, 1, "the controller was halted or reset under firmware"); + assert!(!host.1.borrow().iter().any(|l| l.contains("SMI generation off")), "{:#?}", host.1.borrow()); +} + +/// A controller that issues only 32-bit addresses (HCCPARAMS1.AC64 clear) is +/// driven, and states that width for every grant and lend (§5.3.6). +#[test] +fn a_controller_without_ac64_is_driven_at_32_bits() { + let w = World::new(vec![(FakePort::empty(QUICK), None)], 1); + w.xhc().ac64 = false; + let reset = Reset::new(w.regs.clone(), &w.host).unwrap_or_else(|why| panic!("refused: {why}")); + assert_eq!(reset.address_bits, 32); + let w = World::new(vec![(FakePort::empty(QUICK), None)], 1); + assert_eq!(Reset::new(w.regs.clone(), &w.host).expect("the stub resets").address_bits, 64); +} + +/// The stick present at bring-up is read and offered: its table through the +/// reader the caller gives, and the bus is settled only once it has been. +#[test] +fn a_stick_is_offered_once_its_table_is_read_and_the_bus_settles_only_then() { + let (stick, _, _) = stick("SERIAL1"); + let w = World::new(vec![(FakePort::occupied(QUICK), Some(Box::new(stick)))], 1); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + let mut settled_before = false; + let mut arrived = None; + for step in 1..=60 { + w.run(&usb, &mut tasks, &mut owner, step * 10 * MS).unwrap(); + for event in usb.disks.take_events() { + if let Event::Arrived { disk, table, .. } = event { + arrived = Some((disk, table)); + } + } + if arrived.is_none() && usb.settled() { + settled_before = true; + } + } + assert!(!settled_before, "the bus said it was settled before its disk was read"); + assert!(usb.settled()); + assert_eq!(arrived, Some((0, Some((2048, 8192))))); +} + +/// A session's read and write move straight between the stick and the +/// client's lent region, and a flush is SYNCHRONIZE CACHE. +#[test] +fn a_disks_work_moves_between_the_stick_and_the_lent_region() { + let (stick, disk, _) = stick("SERIAL1"); + let commands = stick.commands.clone(); + let w = World::new(vec![(FakePort::occupied(QUICK), Some(Box::new(stick)))], 1); + w.xhc().mem.borrow_mut().lend(LENT_BASE, 128 * 1024); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 600 * MS).unwrap(); + assert!(matches!(usb.disks.take_events()[..], [Event::Arrived { disk: 0, .. }])); + let pattern: Vec = (0..8192u32).map(|i| (i * 7) as u8).collect(); + w.xhc().mem.borrow_mut().write(LENT_BASE + 0xF000, &pattern); + // Two blocks from 60 KiB into the region: the data crosses 64 KiB. + usb.disks.submit(0, Work::Io { write: true, block: 300, blocks: 2, at: LENT_BASE + 0xF000, token: (1, 1) }); + usb.disks.submit(0, Work::Flush { token: (1, 2) }); + usb.disks.submit(0, Work::Io { write: false, block: 300, blocks: 2, at: LENT_BASE + 0x1_8000, token: (1, 3) }); + w.run(&usb, &mut tasks, &mut owner, 700 * MS).unwrap(); + let answered: Vec<((u64, u32), bool)> = usb + .disks + .take_events() + .into_iter() + .filter_map(|e| match e { + Event::Answered { token, ok, .. } => Some((token, ok)), + _ => None, + }) + .collect(); + assert_eq!(answered, [((1, 1), true), ((1, 2), true), ((1, 3), true)]); + assert_eq!(&disk.borrow()[300 * 4096..302 * 4096], &pattern[..]); + let mut back = vec![0; 8192]; + w.xhc().mem.borrow_mut().read(LENT_BASE + 0x1_8000, &mut back); + assert_eq!(back, pattern); + assert!(commands.borrow().contains(&0x35)); + assert_eq!(w.xhc().mem.borrow().faults, 0); +} + +/// Writes that each cross 64 KiB, so their data phase is a TD of two TRBs, +/// interleaved with reads, whose bulk-out share is their CBW alone: the +/// bulk-out ring laps with TDs at every offset from its Link, and the stub +/// refuses any TD that crosses it. +#[test] +fn a_td_that_would_cross_the_rings_link_starts_behind_it() { + let (stick, disk, _) = stick("SERIAL1"); + let w = World::new(vec![(FakePort::occupied(QUICK), Some(Box::new(stick)))], 1); + w.xhc().mem.borrow_mut().lend(LENT_BASE, 128 * 1024); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 600 * MS).unwrap(); + assert!(matches!(usb.disks.take_events()[..], [Event::Arrived { disk: 0, .. }])); + let pattern: Vec = (0..8192u32).map(|i| (i * 13) as u8).collect(); + w.xhc().mem.borrow_mut().write(LENT_BASE + 0xF000, &pattern); + let works = 400u32; + let (mut queued, mut ok, mut until) = (0u32, 0usize, 600 * MS); + while ok < works as usize { + while queued < works && usb.disks.has_room(0) { + let write = queued % 2 == 0; + let (block, blocks, at) = if write { (300, 2, LENT_BASE + 0xF000) } else { (310, 1, LENT_BASE + 0x1_8000) }; + usb.disks.submit(0, Work::Io { write, block, blocks, at, token: (1, queued) }); + queued += 1; + } + until += 50 * MS; + assert!(until < 60_000 * MS, "{ok} of {works} answered"); + w.run(&usb, &mut tasks, &mut owner, until).unwrap(); + ok += usb.disks.take_events().iter().filter(|e| matches!(e, Event::Answered { ok: true, .. })).count(); + } + assert_eq!(&disk.borrow()[300 * 4096..302 * 4096], &pattern[..]); + assert_eq!(w.xhc().mem.borrow().faults, 0); +} + +/// TD Size (§4.11.2.4): the packets left after each TRB, 0 in the last. +#[test] +fn a_tds_size_counts_the_packets_left_after_each_trb() { + let mps = |n| core::num::NonZeroU16::new(n).unwrap(); + let sizes = |pieces: &[(u64, u32)], max_packet| -> Vec { + hc::normal_td(pieces, mps(max_packet)).iter().map(|t| t.status >> 17).collect() + }; + // 8 KiB from 60 KiB at 512 bytes a packet: 16 packets, 8 after the first. + assert_eq!(sizes(&[(0xF000, 0x1000), (0x1_0000, 0x1000)], 512), [8, 0]); + // 64 KiB from 512 bytes in at 1024: 64 packets, and the first TRB's 63 + // whole ones leave 1 (its residue begins the next). + assert_eq!(sizes(&[(0x200, 0xFE00), (0x1_0000, 0x200)], 1024), [1, 0]); + // More than 31 left is 31. + assert_eq!(sizes(&[(0, 0x1000), (0x1000, 0x1_0000), (0x1_1000, 0x1000)], 512), [31, 8, 0]); + // Every TRB but the last chains; the last interrupts. + let trbs = hc::normal_td(&[(0, 0x1000), (0x1000, 0x1000)], mps(512)); + assert_eq!(trbs.iter().map(|t| (t.control & hc::TRB_CHAIN != 0, t.control & hc::TRB_IOC != 0)).collect::>(), [(true, false), (false, true)]); +} + +/// A controller that answers no command once a device has left: the +/// teardown's Disable Slot is aborted on its deadline (§4.6.1.2), the port is +/// released and can enumerate again, and the slot's pages stay the +/// controller's. +#[test] +fn a_controller_that_never_answers_still_releases_the_port() { + let (kb, _) = keyboard(); + let w = World::new(vec![(FakePort::occupied(QUICK), Some(kb))], 1); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 400 * MS).unwrap(); + let free = usb.hc.pages_free(); + w.xhc().answers = Answers::Never; + w.xhc().ports[0].detach(); + let left = w.host.now.get(); + w.run(&usb, &mut tasks, &mut owner, left + 100 * MS + hc::COMMAND_NS - 10 * MS).unwrap(); + assert!(usb.named.borrow().contains_key(&0), "released before the abort"); + w.run(&usb, &mut tasks, &mut owner, left + 100 * MS + hc::COMMAND_NS + 100 * MS).unwrap(); + assert!(!usb.named.borrow().contains_key(&0), "the port was never released"); + assert_eq!(usb.hc.counts.get().aborted, 1); + assert_eq!(usb.hc.pages_free(), free, "a slot the controller never disabled gave its pages back"); + assert!(owner.got.iter().any(|r| matches!(r, Record::Gone { .. })), "the keyboard's gone was never sent"); +} + +/// An abort the controller does not answer either: the driver ends by name. +#[test] +fn an_unanswered_abort_ends_the_driver_by_name() { + let (kb, _) = keyboard(); + let w = World::new(vec![(FakePort::occupied(QUICK), Some(kb))], 1); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 400 * MS).unwrap(); + w.xhc().answers = Answers::NotEvenAnAbort; + w.xhc().ports[0].detach(); + let ended = w.run(&usb, &mut tasks, &mut owner, 400 * MS + 100 * MS + 2 * hc::COMMAND_NS + 100 * MS); + assert_eq!(ended, Err(Dead("a command and then its abort went unanswered"))); +} + +/// A keyboard whose endpoint stalls, on a controller that refuses the +/// recovery's command: the keyboard is let go of and its `gone` sent, and +/// the port keeps it, so it is not enumerated again every debounce. +#[test] +fn a_recovery_the_controller_refuses_lets_the_device_go() { + struct Stalls(Keyboard); + impl Function for Stalls { + fn control(&mut self, setup: [u8; 8], out: &[u8]) -> crate::stub::Reply { + self.0.control(setup, out) + } + fn transfer(&mut self, _: u8, _: Option<&[u8]>, _: usize) -> crate::stub::Reply { + crate::stub::Reply::Stall + } + } + let (kb, _) = Keyboard::new(); + let w = World::new(vec![(FakePort::occupied(QUICK), Some(Box::new(Stalls(kb))))], 1); + w.xhc().answers = Answers::Refuses(19); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 2_000 * MS).unwrap(); + assert!(w.said("would not run again; it is let go of"), "{:#?}", w.host.lines.borrow()); + assert_eq!(w.xhc().commands.iter().filter(|(_, c)| *c == 11).count(), 1, "enumerated again"); + assert!(usb.named.borrow().contains_key(&0), "the port let its device go"); + assert!(!owner.connected, "the connection outlived its last device"); +} + +/// What a marginal cable does: attach, detach and replug at instants no test +/// author would pick, with every port step and every PORTSC write checked +/// against the invariants, and at the end the driver's belief agreeing with +/// the port. +#[test] +fn no_sequence_breaks_an_invariant() { + for seed in std::env::var("SEED").ok().map_or(0..200u64, |s| { let s: u64 = s.parse().unwrap(); s..s + 1 }) { + let mut state = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1; + let mut next = move || { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + state + }; + let behaviour = if next() % 8 == 0 { ResetBehaviour::Never } else { ResetBehaviour::Completes { after: 1 + next() % (4 * MS) } }; + let (kb, _) = keyboard(); + let w = World::new(vec![(FakePort::empty(behaviour), Some(kb))], 1); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + for _ in 0..16 { + match next() % 4 { + 0 => w.xhc().ports[0].attach(), + 1 => w.xhc().ports[0].detach(), + 2 => w.xhc().ports[0].replug(), + _ => {} + } + let now = w.host.now.get(); + w.run(&usb, &mut tasks, &mut owner, now + 1 + next() % (400 * MS)).unwrap(); + } + let now = w.host.now.get(); + w.run(&usb, &mut tasks, &mut owner, now + 2_000 * MS + crate::port::RESET_DEADLINE_NS).unwrap(); + assert_eq!(w.xhc().violations, [], "seed {seed}"); + assert_eq!(w.xhc().mem.borrow().faults, 0, "seed {seed}"); + let present = w.xhc().ports[0].present(); + if behaviour != ResetBehaviour::Never { + assert_eq!(usb.named.borrow().contains_key(&0), present, "seed {seed}: {:#?}", w.host.lines.borrow()); + } + } +} + +/// Every TRB the driver hands the controller is published body first: no +/// control word, which carries the cycle bit, is stored before a `publish` +/// that follows the TRB's body (§4.9.2). The ARM track's ordering check. +#[test] +fn the_cycle_bit_is_published_after_the_trbs_body() { + let (kb, reports) = keyboard(); + let (stick, _, _) = stick("SERIAL1"); + let w = World::new(vec![(FakePort::occupied(QUICK), Some(kb)), (FakePort::occupied(QUICK), Some(Box::new(stick)))], 2); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 500 * MS).unwrap(); + reports.borrow_mut().push_back(vec![0, 0, 4, 0, 0, 0, 0, 0]); + w.run(&usb, &mut tasks, &mut owner, 600 * MS).unwrap(); + // A ring is a page whose last TRB is its Link back (§4.11.5.1). + let ring = |at: usize| { + let page = at & !0xFFF; + let mut control = [0u8; 4]; + w.xhc().mem.borrow_mut().read(crate::stub::GRANT_BASE + (page + 0xFFC) as u64, &mut control); + (u32::from_le_bytes(control) >> 10) & 0x3F == 6 + }; + let log = w.dma.log.borrow(); + let mut body_since_publish = std::collections::BTreeSet::::new(); + let mut checked = 0; + for entry in log.iter() { + match entry { + None => body_since_publish.clear(), + Some(at) if at % 16 == 12 && ring(*at) => { + let trb = at - 12; + assert!(!body_since_publish.contains(&trb) && !body_since_publish.contains(&(trb + 8)), "the TRB at {trb:#x}'s control word was stored before its body was published"); + checked += 1; + } + Some(at) => { + body_since_publish.insert(*at & !7); + } + } + } + assert!(checked > 20, "{checked} control words"); +} + +/// A write whose CBW goes unanswered leaves the stick owed its Data-Out, so +/// the ladder enters at its port reset (BOT 1.0 §6.7). Answers the driver, +/// the stick's pipes and when the write was queued. +fn a_write_breaks_into_a_port_reset() -> (World, Rc>) { + let (stick, _, pipes) = stick("SERIAL1"); + let w = World::new(vec![(FakePort::occupied(QUICK), Some(Box::new(stick)))], 1); + w.xhc().mem.borrow_mut().lend(LENT_BASE, 64 * 1024); + (w, pipes) +} + +fn answered(usb: &Driver) -> (Vec, Vec>) { + let (mut answers, mut rest) = (Vec::new(), Vec::new()); + for event in usb.disks.take_events() { + match event { + Event::Answered { token, ok, .. } => answers.push((token, ok)), + other => rest.push(other), + } + } + (answers, rest) +} + +/// The same stick back from the port reset its ladder asked for keeps its +/// disk: no `Gone`, and the write it was given is issued again and answered. +#[test] +fn a_stick_back_from_its_port_reset_keeps_its_disk_and_its_work() { + let (w, pipes) = a_write_breaks_into_a_port_reset(); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 600 * MS).unwrap(); + assert!(matches!(answered(&usb).1[..], [Event::Arrived { disk: 0, .. }])); + pipes.set(Pipes::SilentOut); + let queued = w.host.now.get(); + usb.disks.submit(0, Work::Io { write: true, block: 10, blocks: 1, at: LENT_BASE, token: (1, 1) }); + w.run(&usb, &mut tasks, &mut owner, queued + crate::msc::COMMAND_NS + MS / 2).unwrap(); + assert!(w.said("the ladder climbs to its port reset"), "{:#?}", w.host.lines.borrow()); + pipes.set(Pipes::Answer); + w.run(&usb, &mut tasks, &mut owner, queued + crate::msc::COMMAND_NS + 600 * MS).unwrap(); + let (answers, rest) = answered(&usb); + assert!(w.said("disk 0's device came back"), "{:#?}", w.host.lines.borrow()); + assert_eq!(answers, [((1, 1), true)]); + assert!(rest.is_empty(), "{rest:?}"); +} + +/// A different stick on the port after that reset is a new disk, and the +/// one that left is held for its return window and then ends, its write +/// answered not done and never issued to the stranger. +#[test] +fn a_different_stick_after_the_port_reset_ends_the_disk_that_left() { + let (w, pipes) = a_write_breaks_into_a_port_reset(); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 600 * MS).unwrap(); + answered(&usb); + pipes.set(Pipes::SilentOut); + let queued = w.host.now.get(); + usb.disks.submit(0, Work::Io { write: true, block: 10, blocks: 1, at: LENT_BASE, token: (1, 1) }); + w.run(&usb, &mut tasks, &mut owner, queued + crate::msc::COMMAND_NS + MS / 2).unwrap(); + let (other, other_disk, _) = stick("OTHER"); + w.xhc().devices[0] = Some(Box::new(other)); + w.run(&usb, &mut tasks, &mut owner, queued + crate::msc::COMMAND_NS + crate::identity::RETURN_WINDOW + 100 * MS).unwrap(); + let (answers, rest) = answered(&usb); + assert_eq!(answers, [((1, 1), false)]); + assert!(matches!(rest[..], [Event::Arrived { disk: 1, .. }, Event::Gone { disk: 0 }]), "{rest:?}"); + assert_eq!(&other_disk.borrow()[10 * 4096..11 * 4096], &[0u8; 4096][..], "the write reached the stranger"); +} + +/// A SuperSpeed link whose port is reset the way it cannot take — a hot +/// reset that kills it, or one that completes failed (§4.19.5) — is given +/// the warm reset §4.19.1.2.4 has for it, and its device is enumerated. +#[test] +fn a_superspeed_link_a_hot_reset_breaks_is_recovered_warm() { + for behaviour in [ + ResetBehaviour::HotResetKillsTheLink { warm_works: true }, + ResetBehaviour::FailsTheBusReset { warm_works: true }, + ] { + let (kb, _) = keyboard(); + let w = World::new(vec![(FakePort::untrained(behaviour), Some(kb))], 0); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 4 * crate::port::RESET_DEADLINE_NS).unwrap(); + assert!(w.said("port 1 Warm reset"), "{behaviour:?}: {:#?}", w.host.lines.borrow()); + assert!(usb.named.borrow().contains_key(&0), "{behaviour:?}: {:#?}", w.host.lines.borrow()); + assert_eq!(w.xhc().violations, [], "{behaviour:?}"); + } +} + +/// A link a warm reset re-detects and still cannot bring up is given up on +/// by its own name, and not reset again while its device stays in. +#[test] +fn a_link_that_never_trains_is_given_up_on_and_left_alone() { + let (kb, _) = keyboard(); + let w = World::new(vec![(FakePort::untrained(ResetBehaviour::RetrainsDisabled), Some(kb))], 0); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 4 * crate::port::RESET_DEADLINE_NS).unwrap(); + assert!(w.said("given up on: LinkNeverTrained"), "{:#?}", w.host.lines.borrow()); + let resets = w.host.lines.borrow().iter().filter(|l| l.contains("reset")).count(); + w.run(&usb, &mut tasks, &mut owner, 20 * crate::port::RESET_DEADLINE_NS).unwrap(); + assert_eq!(w.host.lines.borrow().iter().filter(|l| l.contains("reset")).count(), resets, "reset again"); + assert!(usb.settled(), "a port given up on keeps the bus from settling"); +} + +/// An interrupt whose event an earlier drain already took — a controller's +/// assertion that landed inside a drain — finds the ring empty, and the next +/// event is still announced: every drain ends Event Handler Busy, or no event +/// after it raises an interrupt and the driver waits for ever. +#[test] +fn an_interrupt_that_finds_no_event_leaves_the_next_one_announced() { + let (kb, reports) = keyboard(); + let w = World::new(vec![(FakePort::occupied(QUICK), Some(kb))], 1); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 400 * MS).unwrap(); + w.xhc().raise_for_nothing(); + w.run(&usb, &mut tasks, &mut owner, 410 * MS).unwrap(); + reports.borrow_mut().push_back(vec![0, 0, 0x04, 0, 0, 0, 0, 0]); + w.run(&usb, &mut tasks, &mut owner, 420 * MS).unwrap(); + assert_eq!(keys(&owner.got), [(0x04, true)], "the report's event raised no interrupt"); +} + +/// A stick that answers late, past the transfer's bound, and then answers +/// again: the ladder takes the transport back, the work it was given is +/// answered, and work queued after it is served. +#[test] +fn a_stick_that_answers_late_once_is_served_again() { + let (stick, _, pipes) = stick("SERIAL1"); + let w = World::new(vec![(FakePort::occupied(QUICK), Some(Box::new(stick)))], 1); + w.xhc().mem.borrow_mut().lend(LENT_BASE, 64 * 1024); + let usb = w.start(BOTH); + let mut tasks = usb.tasks(); + let _end = End(&usb); + let mut owner = Mock::default(); + w.run(&usb, &mut tasks, &mut owner, 600 * MS).unwrap(); + answered(&usb); + pipes.set(Pipes::SilentIn); + let queued = w.host.now.get(); + usb.disks.submit(0, Work::Io { write: true, block: 10, blocks: 1, at: LENT_BASE, token: (1, 1) }); + usb.disks.submit(0, Work::Io { write: false, block: 10, blocks: 1, at: LENT_BASE + 4096, token: (1, 2) }); + w.run(&usb, &mut tasks, &mut owner, queued + crate::msc::COMMAND_NS + 2_000 * MS).unwrap(); + pipes.set(Pipes::Answer); + w.run(&usb, &mut tasks, &mut owner, queued + 6 * crate::msc::COMMAND_NS).unwrap(); + let (answers, rest) = answered(&usb); + assert_eq!(answers, [((1, 1), true), ((1, 2), true)], "{rest:?}\n{:#?}", w.host.lines.borrow()); + usb.disks.submit(0, Work::Flush { token: (1, 3) }); + let now = w.host.now.get(); + w.run(&usb, &mut tasks, &mut owner, now + 1_000 * MS).unwrap(); + assert_eq!(answered(&usb).0, [((1, 3), true)]); +} diff --git a/system/usbd/xhci/src/usb.rs b/system/usbd/xhci/src/usb.rs new file mode 100644 index 00000000000..3f3e61c29db --- /dev/null +++ b/system/usbd/xhci/src/usb.rs @@ -0,0 +1,997 @@ +//! The root hub and what is on it: one task per port, each following its +//! port through `port::PortState`, enumerating what connects in xHCI 1.2 +//! §4.3's order, and serving the device's functions until it leaves. +//! +//! **Boot discovery is hot-plug.** Every port's task starts at bring-up; a +//! device present then and one plugged in later run the same code. A port +//! found connected is reset before its device is asked anything +//! (`PortState::adopt`). +//! +//! **A device's functions run beside its port's watch.** Each bound +//! interface is one future, and the port's task polls them with its own +//! look at the port; a teardown disables the slot, which answers every +//! operation still held on it [`Done::Gone`], and the functions end on that. + +use alloc::boxed::Box; +use alloc::collections::BTreeMap; +use alloc::string::String; +use alloc::vec::Vec; +use core::cell::{Cell, RefCell}; +use core::future::{poll_fn, Future}; +use core::pin::pin; +use core::task::Poll; + +use toyos_device_memory::{DmaBuffers, Registers}; + +use crate::configure::{self, Configure}; +use crate::descriptor::{self, Configuration, Endpoint}; +use crate::exec::{self, Dead, Host, Nanos, Task, Tasks, Wake}; +use crate::hc::{self, Done, Hc, Op, Ring, Trb}; +use crate::input::Inputs; +use crate::msc::Disks; +use crate::port::{self, GaveUp, Gone, PortState, Reset, Step}; +use crate::protocol::Protocol; +use crate::recovery::{Act, Command, EndpointState, Recovery}; + +/// TRSTRCY: "The USB System Software guarantees a minimum of 10 ms for reset +/// recovery" (USB 2.0 §7.1.7.3, §7.1.7.5), which xHCI leaves to software +/// (xHCI 1.2 §4.6.8). A fixed delay the specification mandates and raises no +/// event for. USB 3.2 §9.2.6.2 lets a SuperSpeed device be accessed at once. +pub(crate) const RESET_RECOVERY_NS: Nanos = 10_000_000; + +/// How long a control transfer may go unanswered: "an upper limit of 5 +/// seconds for any command to be processed" (USB 3.2 §9.2.6.1). +pub const CONTROL_NS: Nanos = 5_000_000_000; + +/// What a configuration descriptor is read with: more than any device in +/// reach answers, and a longer one is walked as the prefix that arrived. +const CONFIG_BYTES: u16 = 1024; + +/// Which classes this usbd binds. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Config { + /// HID interfaces are bound; an owner of input found not to be this + /// usbd's ends their binding (`input::Opened::NotGiven`). + pub input: bool, + /// The row receives `disk`: mass-storage interfaces are bound. + pub disks: bool, +} + +/// A device this controller has enumerated, for `inspect`. +#[derive(Clone, Debug)] +pub struct Named { + pub port: u8, + pub slot: u8, + pub speed: u8, + pub device: descriptor::Device, + /// What each interface was bound as, or why not. + pub functions: Vec, +} + +/// What a function asks of its device's port. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Ask { + /// A port reset and enumeration again: the disk on it is held for its + /// return (`identity::RETURN_WINDOW`). + Reenumerate, + /// The device is let go of, and the last thing it is sent is a reset. + Offline, +} + +/// The driver: the controller, every port's belief, and what is bound. +pub struct Usb { + pub hc: Hc, + pub wake: Wake, + ports: Vec>, + /// Ports connected at bring-up whose first look has not ended. + unsettled: RefCell>, + /// When each port's [`port::attach_window`] ends: an empty port is settled + /// only past it. + attach_by: Vec, + pub named: RefCell>, + pub disks: Disks, + pub input: RefCell, + pub hid: Cell, + pub config: Config, + pub(crate) table: fn(&mut dyn toyos_gpt::Sectors) -> T, + /// The bring-up's No-Op: `None` while it is outstanding. + pub noop: Cell>, + /// A mass-storage device's run of breaks, carried by its port across the + /// port reset its ladder asked for, so a device that never answers comes + /// to its last rung. + pub(crate) runs: RefCell>, +} + +impl Usb { + /// The ports as the controller came up, every connected one given the + /// reset a device left by whatever ran before needs. + pub fn new(hc: Hc, host: C, config: Config, table: fn(&mut dyn toyos_gpt::Sectors) -> T) -> Self { + let wake = Wake::new(host); + let now = wake.now(); + // **Every port is unsettled at the start**, connected or not: the + // ports were powered at `start`, and a device on one may not read + // connected yet. A connected one settles once enumerated or given up + // on; an empty one once its attach window has passed. + let unsettled = alloc::vec![true; usize::from(hc.max_ports)]; + let attach_by = (0..hc.max_ports) + .map(|p| now.saturating_add(port::attach_window(hc.protocols.of(p) == Some(crate::protocol::Protocol::Usb3)))) + .collect(); + let ports = (0..hc.max_ports) + .map(|p| { + let mut port = PortState::EMPTY; + port.speaks(hc.protocols.of(p)); + let portsc = hc.portsc(p); + if portsc.connected() { + let (kind, writes) = port.adopt(portsc, now); + wake.host().say(format_args!( + "port {} found connected (PORTSC {:#010x}); {kind:?} resetting it", + p + 1, + portsc.raw() + )); + for write in writes { + hc.write_portsc(p, write); + } + } + RefCell::new(port) + }) + .collect(); + Self { + hc, + wake, + ports, + unsettled: RefCell::new(unsettled), + attach_by, + named: RefCell::new(BTreeMap::new()), + disks: Disks::default(), + input: RefCell::new(Inputs::default()), + hid: Cell::new(toyos_usbhid::report::Counts::default()), + config, + table, + noop: Cell::new(None), + runs: RefCell::new(BTreeMap::new()), + } + } + + /// Every task the driver runs: the bring-up's No-Op, each port's, and + /// the keeper of disks held for their device's return. + pub fn tasks(&self) -> Tasks<'_> { + let mut tasks = Tasks::default(); + tasks.spawn(self.no_op()); + for p in 0..self.hc.max_ports { + tasks.spawn(self.port(p)); + } + tasks.spawn(self.disks.keep(self)); + tasks + } + + pub fn say(&self, line: core::fmt::Arguments<'_>) { + self.wake.host().say(line); + } + + /// Every port connected at bring-up was enumerated and its device bound + /// or refused, and every disk on them read or refused. + pub fn settled(&self) -> bool { + !self.unsettled.borrow().iter().any(|u| *u) && self.disks.settled() + } + + fn settle(&self, p: u8) { + self.unsettled.borrow_mut()[usize::from(p)] = false; + } + + /// A No-Op that says the command ring and the interrupt both work before + /// any device depends on them. + async fn no_op(&self) -> Result<(), Dead> { + let done = self.hc.command(&self.wake, Trb { control: hc::TRB_NO_OP, ..Trb::default() }).await?; + self.noop.set(Some(done.succeeded())); + match done.succeeded() { + true => self.say(format_args!("a No-Op command completed")), + false => self.say(format_args!("the bring-up's No-Op command: {done:?}")), + } + Ok(()) + } + + /// One port, for the driver's life. + async fn port(&self, p: u8) -> Result<(), Dead> { + loop { + match self.look(p) { + Look::Idle => { + // An empty port: settled once its attach window is over + // with nothing connected, and its next change awaited. + let by = self.attach_by[usize::from(p)]; + if self.unsettled.borrow()[usize::from(p)] && !self.hc.portsc(p).connected() { + if self.wake.now() >= by { + self.settle(p); + } else { + exec::until_or(&self.wake, by, || self.hc.take_change(p).then_some(())).await; + continue; + } + } + exec::until(&self.wake, || self.hc.take_change(p).then_some(())).await + } + Look::Wait(at) => { + exec::until_or(&self.wake, at, || self.hc.take_change(p).then_some(())).await; + } + Look::Again => {} + Look::GaveUp(why) => { + self.say(format_args!("port {}: given up on: {why:?} (PORTSC {:#010x})", p + 1, self.hc.portsc(p).raw())); + self.settle(p); + } + Look::Teardown(gone) => { + // A device refused or let go of: it holds no slot, or one + // nothing serves. + self.say(format_args!("port {}: its device is gone ({gone:?})", p + 1)); + let slot = self.ports[usize::from(p)].borrow_mut().take_slot(); + if let Some(slot) = slot { + self.disable(slot.get(), Vec::new()).await?; + } + self.named.borrow_mut().remove(&p); + self.ports[usize::from(p)].borrow_mut().torn_down(); + } + Look::Enumerate(after) => { + let bound = self.arrived(p, after).await?; + self.settle(p); + if let Some(device) = bound { + self.serve(device).await?; + } + } + } + } + } + + /// What the port asks for now, with its machine's borrow over. + fn look(&self, p: u8) -> Look { + let portsc = self.hc.portsc(p); + let mut port = self.ports[usize::from(p)].borrow_mut(); + let now = self.wake.now(); + #[cfg(test)] + let before = *port; + let step = port.step(portsc, now); + #[cfg(test)] + if let Some(why) = crate::invariants::check(&before, &step, portsc, now) { + panic!("port {}: {why:?} from {before:?} on {portsc:?}", p + 1); + } + match step { + Step::Idle => Look::Idle, + Step::Wait(at) => Look::Wait(at), + Step::Write(write) => { + self.hc.write_portsc(p, write); + Look::Again + } + Step::Reset(kind, write) => { + self.say(format_args!("port {} {kind:?} reset", p + 1)); + self.hc.write_portsc(p, write); + Look::Again + } + Step::GaveUp(why) => Look::GaveUp(why), + Step::Enumerate { after, pending } => { + pending.running(); + Look::Enumerate(after) + } + Step::Teardown(gone, pending) => { + pending.running(); + Look::Teardown(gone) + } + } + } + + /// A port whose reset finished: acknowledge it, enumerate what is there + /// and bind its functions. `None` for a device enumerated nothing was + /// bound on, or one refused; the port keeps it until it is pulled. + async fn arrived(&self, p: u8, after: Option) -> Result>, Dead> { + let portsc = self.hc.portsc(p); + self.hc.write_portsc(p, port::enumeration_ack(after, portsc)); + let portsc = self.hc.portsc(p); + let port = &self.ports[usize::from(p)]; + if !portsc.enabled() { + self.say(format_args!("port {}: reset and not enabled (PORTSC {:#010x})", p + 1, portsc.raw())); + port.borrow_mut().enumerated(None); + return Ok(None); + } + if after == Some(Reset::Hot) && self.hc.protocols.of(p) != Some(Protocol::Usb3) { + exec::sleep_until(&self.wake, self.wake.now() + RESET_RECOVERY_NS).await; + } + let speed = portsc.speed(); + let Some(packet) = initial_ep0_packet(speed) else { + self.say(format_args!("port {}: came up at speed {speed}, which this driver has no default for", p + 1)); + port.borrow_mut().enumerated(None); + return Ok(None); + }; + let done = self.hc.command(&self.wake, Trb { control: hc::TRB_ENABLE_SLOT, ..Trb::default() }).await?; + let Done::Event { code: hc::CC_SUCCESS, slot, .. } = done else { + self.say(format_args!("port {}: Enable Slot: {done:?}; leaving it unnamed", p + 1)); + port.borrow_mut().enumerated(None); + return Ok(None); + }; + let Some(device) = Device::new(self, p, slot, speed, packet) else { + self.say(format_args!("port {}: no room in the grant for slot {slot}; leaving it unnamed", p + 1)); + self.disable(slot, Vec::new()).await?; + port.borrow_mut().enumerated(None); + return Ok(None); + }; + match device.enumerate().await? { + Ok(()) => {} + Err(why) => { + self.say(format_args!("port {}: {why}; leaving it unnamed", p + 1)); + let pages = device.pages.take(); + self.disable(slot, pages).await?; + port.borrow_mut().enumerated(None); + return Ok(None); + } + } + port.borrow_mut().enumerated(core::num::NonZeroU8::new(slot)); + Ok(Some(device)) + } + + /// Serve `device`'s functions until the port takes it away or every one + /// has ended, and then give its slot back. + async fn serve(&self, device: Device<'_, R, D, C, T>) -> Result<(), Dead> { + let p = device.port; + let functions = device.functions(); + let mut running = pin!(exec::join(functions)); + // The port is read against the belief its enumeration left before + // anything else: a pull inside the enumeration raised no edge after it. + let mut wait_at: Option = Some(self.wake.now()); + let mut finished = false; + let ended = loop { + enum Woke { + Ended(Result<(), Dead>), + Asked(Ask), + Look, + } + let woke = poll_fn(|cx| { + if let Poll::Ready(ended) = running.as_mut().poll(cx) { + return Poll::Ready(Woke::Ended(ended)); + } + if let Some(ask) = device.ask.take() { + self.wake.stir(); + return Poll::Ready(Woke::Asked(ask)); + } + if self.hc.take_change(p) || wait_at.is_some_and(|at| self.wake.now() >= at) { + self.wake.stir(); + return Poll::Ready(Woke::Look); + } + if let Some(at) = wait_at { + self.wake.wake_at(at); + } + Poll::Pending + }) + .await; + match woke { + Woke::Ended(ended) => { + ended?; + finished = true; + // A function that asked its port for something ends on + // asking. + break device.ask.take().map(Leave::Asked); + } + Woke::Asked(ask) => break Some(Leave::Asked(ask)), + Woke::Look => match self.look(p) { + Look::Idle => wait_at = None, + Look::Wait(at) => wait_at = Some(at), + Look::Again => wait_at = Some(self.wake.now()), + Look::Teardown(gone) => break Some(Leave::Gone(gone)), + look @ (Look::GaveUp(_) | Look::Enumerate(_)) => { + unreachable!("a port holding a device it enumerated asked {look:?}") + } + }, + } + }; + let slot = self.ports[usize::from(p)].borrow_mut().take_slot().map_or(device.slot, |s| s.get()); + match ended { + None => self.say(format_args!("port {}: nothing on its device is served now; it keeps the port", p + 1)), + Some(Leave::Gone(gone)) => self.say(format_args!("port {}: its device is gone ({gone:?})", p + 1)), + Some(Leave::Asked(ask)) => self.say(format_args!("port {}: its device is reset ({ask:?})", p + 1)), + } + device.leaving.set(true); + let pages = device.pages.take(); + // The functions are polled while the slot goes: one may hold the + // command ring the Disable Slot waits its turn on. Every operation on + // the slot is answered once it has gone, so every function ends. + let mut disabling = pin!(self.disable(slot, pages)); + let mut disabled = false; + poll_fn(|cx| { + if !disabled + && let Poll::Ready(done) = disabling.as_mut().poll(cx) { + disabled = true; + if let Err(dead) = done { + return Poll::Ready(Err(dead)); + } + } + if !finished + && let Poll::Ready(done) = running.as_mut().poll(cx) { + finished = true; + if let Err(dead) = done { + return Poll::Ready(Err(dead)); + } + } + if disabled && finished { Poll::Ready(Ok(())) } else { Poll::Pending } + }) + .await?; + let mut port = self.ports[usize::from(p)].borrow_mut(); + match ended { + Some(Leave::Gone(_)) => { + self.named.borrow_mut().remove(&p); + port.torn_down(); + } + Some(Leave::Asked(Ask::Reenumerate)) => { + self.named.borrow_mut().remove(&p); + let portsc = self.hc.portsc(p); + let (_, writes) = port.adopt(portsc, self.wake.now()); + for write in writes { + self.hc.write_portsc(p, write); + } + } + Some(Leave::Asked(Ask::Offline)) => { + let portsc = self.hc.portsc(p); + let kind = port::offline_reset(self.hc.protocols.of(p)); + self.hc.write_portsc(p, port::reset_write(kind, portsc)); + } + // Its functions ended by themselves: it stays in the port, which + // looks for its pull. + None => {} + } + Ok(()) + } + + /// Disable `slot` and give back its pages; a Disable Slot the controller + /// would not answer keeps them, since it may still reach them. + async fn disable(&self, slot: u8, pages: Vec) -> Result<(), Dead> { + let trb = Trb { control: hc::TRB_DISABLE_SLOT | (u32::from(slot) << 24), ..Trb::default() }; + let done = self.hc.command(&self.wake, trb).await?; + self.hc.slot_gone(slot); + if done.succeeded() { + self.hc.name_slot(slot, None); + self.hc.release(pages); + } else { + self.say(format_args!( + "Disable Slot {slot}: {done:?}; its {} page(s) stay the controller's", + pages.len() + )); + } + Ok(()) + } +} + +enum Leave { + Gone(Gone), + Asked(Ask), +} + +/// What a port asks for, with its machine's borrow over. +#[derive(Debug)] +enum Look { + Idle, + Wait(Nanos), + Again, + GaveUp(GaveUp), + Enumerate(Option), + Teardown(Gone), +} + +/// EP0's Max Packet Size before the device has been asked, and `None` for a +/// speed this driver has no encoding for. +/// +/// Low, High and SuperSpeed each fix it — 8, 64 and 512. **Full Speed does +/// not**: `bMaxPacketSize0` is 8, 16, 32 or 64 there, unknown until the first +/// eight bytes of the device descriptor are read, and every device answers +/// those in one packet (USB 2.0 §5.5.3), so 8 is what Address Device carries +/// and what Evaluate Context replaces (xHCI 1.2 §4.3, step 7). +pub fn initial_ep0_packet(speed: u8) -> Option { + match speed { + 1 | 2 => Some(8), + 3 => Some(64), + 4..=7 => Some(512), + _ => None, + } +} + +/// What `bMaxPacketSize0` means at this speed, or `None` when the device named +/// a size a device of that speed does not have. SuperSpeed states the +/// exponent: the byte is 9 and the size is 512 (USB 3.2 §9.6.1). +pub fn ep0_packet_from_descriptor(speed: u8, stated: u8) -> Option { + match (speed, stated) { + (1, 8 | 16 | 32 | 64) => Some(u16::from(stated)), + (2, 8) => Some(8), + (3, 64) => Some(64), + (4..=7, 9) => Some(512), + _ => None, + } +} + +/// A port speed's name (xHCI 1.2 Table 7-13's default Protocol Speed IDs). +pub fn speed_name(speed: u8) -> &'static str { + match speed { + 1 => "full", + 2 => "low", + 3 => "high", + 4 => "super", + 5 => "super-plus-gen2x1", + 6 => "super-plus-gen1x2", + 7 => "super-plus-gen2x2", + _ => "unknown", + } +} + +/// A standard, class or vendor request (USB 2.0 §9.3). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Setup { + pub request_type: u8, + pub request: u8, + pub value: u16, + pub index: u16, + pub length: u16, +} + +impl Setup { + fn packet(self) -> u64 { + u64::from(self.request_type) + | u64::from(self.request) << 8 + | u64::from(self.value) << 16 + | u64::from(self.index) << 32 + | u64::from(self.length) << 48 + } + + fn data_in(self) -> bool { + self.request_type & 0x80 != 0 + } + + pub const fn get_descriptor(kind: u8, index: u8, length: u16) -> Self { + Self { request_type: 0x80, request: 6, value: (kind as u16) << 8 | index as u16, index: 0, length } + } +} + +/// How a transfer the device did not complete ended. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Failed { + /// It stalled: the device refused the request. + Stall, + /// The controller ended it with this completion code. + Code(u32), + /// Nothing answered by its deadline, and the controller gave it back. + Silent, + /// The device left. + Gone, + /// The endpoint would not run again. + Stuck, +} + +/// One enumerated device: its slot, its contexts and rings, and its +/// configuration. +pub struct Device<'u, R: Registers, D: DmaBuffers, C: Host, T> { + pub usb: &'u Usb, + pub port: u8, + pub slot: u8, + pub speed: u8, + packet: Cell, + out: usize, + input: usize, + /// EP0's data stage lands here. + data: usize, + ep0: RefCell, + ep0_busy: Cell, + rings: RefCell>, + /// Every page the device holds, given back with its slot. + pub pages: RefCell>, + pub descriptor: Cell>, + pub config: RefCell>, + pub ask: Cell>, + /// Its port took it away: a function issues nothing more. + pub leaving: Cell, +} + +impl<'u, R: Registers, D: DmaBuffers, C: Host, T> Device<'u, R, D, C, T> { + fn new(usb: &'u Usb, port: u8, slot: u8, speed: u8, packet: u16) -> Option { + let hc = &usb.hc; + let mut pages = Vec::new(); + let mut take = || -> Option { + let page = hc.page()?; + pages.push(page); + Some(page) + }; + let (out, input, ring, data) = match (take(), take(), take(), take()) { + (Some(out), Some(input), Some(ring), Some(data)) => (out, input, ring, data), + _ => { + hc.release(pages); + return None; + } + }; + hc.name_slot(slot, Some(out)); + let ep0 = hc.ring(ring); + Some(Self { + usb, + port, + slot, + speed, + packet: Cell::new(packet), + out, + input, + data, + ep0: RefCell::new(ep0), + ep0_busy: Cell::new(false), + rings: RefCell::new(BTreeMap::new()), + pages: RefCell::new(pages), + descriptor: Cell::new(None), + config: RefCell::new(None), + ask: Cell::new(None), + leaving: Cell::new(false), + }) + } + + pub fn wake(&self) -> &'u Wake { + &self.usb.wake + } + + pub fn hc(&self) -> &'u Hc { + &self.usb.hc + } + + /// A zeroed page of the grant this device holds until its slot goes. + pub fn page(&self) -> Option { + let page = self.hc().page()?; + self.pages.borrow_mut().push(page); + Some(page) + } + + fn input_word(&self, context: usize, dword: usize, value: u32) { + self.hc().dma.write32(self.hc().context(self.input, context) + dword * 4, value); + } + + fn clear_input(&self) { + for off in (0..hc::PAGE).step_by(8) { + self.hc().dma.write64(self.input + off, 0); + } + } + + /// A command that names this slot's input context. + async fn input_command(&self, kind: u32) -> Result { + self.hc().dma.publish(); + let trb = Trb { param: self.hc().dma.device_addr(self.input), status: 0, control: kind | u32::from(self.slot) << 24 }; + self.hc().command(self.wake(), trb).await + } + + /// xHCI 1.2 §4.3's order up to the configuration: Address Device; at full + /// speed, the descriptor's first eight bytes and Evaluate Context; the + /// device descriptor; the configuration. + async fn enumerate(&self) -> Result, Dead> { + let ep0 = |packet: u16| (3u32 << 1) | (4u32 << 3) | (u32::from(packet) << 16); + self.clear_input(); + // Add the slot and EP0 (§4.3.3): one context entry, the speed and the + // root-hub port. + self.input_word(0, 1, 0b11); + self.input_word(1, 0, (u32::from(self.speed) << 20) | (1 << 27)); + self.input_word(1, 1, (u32::from(self.port) + 1) << 16); + let dequeue = self.ep0.borrow().dequeue(&self.hc().dma); + self.input_word(2, 1, ep0(self.packet.get())); + self.input_word(2, 2, dequeue as u32); + self.input_word(2, 3, (dequeue >> 32) as u32); + self.input_word(2, 4, 8); + let done = self.input_command(hc::TRB_ADDRESS_DEVICE).await?; + if !done.succeeded() { + return Ok(Err(alloc::format!("Address Device: {done:?}"))); + } + if self.speed == 1 { + let prefix = match self.control(Setup::get_descriptor(1, 0, 8)).await? { + Ok(bytes) => bytes, + Err(why) => return Ok(Err(alloc::format!("the device descriptor's first 8 bytes: {why:?}"))), + }; + let stated = match descriptor::ep0_packet(&prefix) { + Ok(stated) => stated, + Err(why) => return Ok(Err(alloc::format!("the device descriptor's first 8 bytes: {why}"))), + }; + let Some(packet) = ep0_packet_from_descriptor(self.speed, stated) else { + return Ok(Err(alloc::format!("bMaxPacketSize0={stated} is no size a full-speed device has"))); + }; + if packet != self.packet.get() { + // EP0's packet size, and nothing else (§4.6.7). + self.clear_input(); + self.input_word(0, 1, 1 << 1); + self.input_word(2, 1, ep0(packet)); + let done = self.input_command(hc::TRB_EVALUATE_CONTEXT).await?; + if !done.succeeded() { + return Ok(Err(alloc::format!("Evaluate Context: {done:?}"))); + } + self.packet.set(packet); + } + } + let device = match self.control(Setup::get_descriptor(1, 0, descriptor::DEVICE_BYTES as u16)).await? { + Ok(bytes) => match descriptor::device(&bytes) { + Ok(device) => device, + Err(why) => return Ok(Err(alloc::format!("the device descriptor: {why}"))), + }, + Err(why) => return Ok(Err(alloc::format!("the device descriptor: {why:?}"))), + }; + self.descriptor.set(Some(device)); + let config = match self.control(Setup::get_descriptor(2, 0, CONFIG_BYTES)).await? { + Ok(bytes) => match descriptor::configuration(&bytes) { + Ok(config) => config, + Err(why) => return Ok(Err(alloc::format!("the configuration: {why}"))), + }, + Err(why) => return Ok(Err(alloc::format!("the configuration: {why:?}"))), + }; + *self.config.borrow_mut() = Some(config); + Ok(Ok(())) + } + + /// Bind what this usbd binds of the configuration: create the endpoints + /// every bound interface uses with one Configure Endpoint, select the + /// configuration (§4.3.5), and answer each function's future. + fn functions(&self) -> Vec> { + let usb = self.usb; + let config = self.config.borrow().clone().expect("enumerated"); + let device = self.descriptor.get().expect("enumerated"); + let mut bound: Vec<(descriptor::Interface, Function, Option>)> = Vec::new(); + let mut said = Vec::new(); + for interface in &config.interfaces { + let class = interface.class; + let function = match (class.class, class.subclass, class.protocol) { + (3, ..) if usb.config.input => Ok(Function::Hid), + (3, ..) => Err("HID, and this usbd receives no `input`"), + (8, 6, 0x50) if usb.config.disks => Ok(Function::Disk), + (8, 6, 0x50) => Err("mass storage, and this usbd receives no `disk`"), + (8, ..) => Err("mass storage other than SCSI over Bulk-Only"), + _ => Err("a class this driver does not bind"), + }; + match function { + Ok(function) => { + said.push(alloc::format!("interface {}: {function:?}", interface.number)); + // Counted from here, so the bus is not called settled + // between its bind and its table's read. + let binding = (function == Function::Disk).then(|| usb.disks.binding()); + bound.push((interface.clone(), function, binding)); + } + Err(why) => said.push(alloc::format!( + "interface {} ({:02x}:{:02x}:{:02x}): not bound, {why}", + interface.number, + class.class, + class.subclass, + class.protocol + )), + } + } + usb.say(format_args!( + "port {}: {:04x}:{:04x} at {} speed on slot {}: {}", + self.port + 1, + device.vendor, + device.product, + speed_name(self.speed), + self.slot, + said.join("; ") + )); + usb.named.borrow_mut().insert( + self.port, + Named { port: self.port, slot: self.slot, speed: self.speed, device, functions: said }, + ); + let mut all: Vec> = Vec::new(); + if bound.is_empty() { + return all; + } + all.push(Box::pin(async move { + let endpoints: Vec = bound.iter().flat_map(|(i, ..)| i.endpoints.iter().copied()).collect(); + if let Err(why) = self.configure(&endpoints, Configure::First { speed: self.speed, port: self.port + 1 }).await? { + usb.say(format_args!("port {}: {why}; nothing on it is bound", self.port + 1)); + return Ok(()); + } + let select = Setup { request_type: 0, request: 9, value: u16::from(config.value), index: 0, length: 0 }; + if let Err(why) = self.control(select).await? { + usb.say(format_args!("port {}: SET_CONFIGURATION({}): {why:?}; nothing on it is bound", self.port + 1, config.value)); + return Ok(()); + } + let functions: Vec> = bound + .into_iter() + .map(|(interface, function, binding)| -> Task<'_> { + match (function, binding) { + (Function::Disk, Some(binding)) => Box::pin(crate::msc::serve(self, interface, binding)), + _ => Box::pin(crate::hid::serve(self, interface)), + } + }) + .collect(); + exec::join(functions).await + })); + all + } + + /// Create `endpoints`, each on a fresh ring (§4.6.6), or re-create them + /// with their toggles zeroed. + pub async fn configure(&self, endpoints: &[Endpoint], how: Configure) -> Result, Dead> { + let mut contexts = Vec::new(); + for ep in endpoints { + let kind = match (ep.kind, ep.is_in()) { + (descriptor::BULK, true) => configure::Kind::BulkIn, + (descriptor::BULK, false) => configure::Kind::BulkOut, + (descriptor::INTERRUPT, true) => configure::Kind::InterruptIn, + (descriptor::INTERRUPT, false) => configure::Kind::InterruptOut, + (kind, _) => return Ok(Err(alloc::format!("endpoint {:#04x} is of transfer type {kind}, which nothing here drives", ep.address))), + }; + let page = match self.rings.borrow().get(&ep.dci()) { + Some(ring) => ring.at, + None => match self.page() { + Some(page) => page, + None => return Ok(Err("the grant has no page left for an endpoint's ring".into())), + }, + }; + let ring = self.hc().ring(page); + let dequeue = ring.dequeue(&self.hc().dma); + self.rings.borrow_mut().insert(ep.dci(), ring); + let interval = if kind == configure::Kind::InterruptIn || kind == configure::Kind::InterruptOut { + configure::interval(self.speed, ep.interval) + } else { + 0 + }; + contexts.push(configure::Endpoint { + dci: ep.dci(), + kind, + max_packet: ep.max_packet, + max_burst: ep.max_burst, + interval, + dequeue, + }); + } + self.clear_input(); + for word in configure::endpoints(&contexts, how) { + self.input_word(word.context, word.dword, word.value); + } + let done = self.input_command(hc::TRB_CONFIGURE_ENDPOINT).await?; + Ok(if done.succeeded() { Ok(()) } else { Err(alloc::format!("Configure Endpoint: {done:?}")) }) + } + + /// One request on EP0, its data stage, if any, device to host into the + /// device's data page: the bytes that arrived. + pub async fn control(&self, setup: Setup) -> Result, Failed>, Dead> { + exec::until(self.wake(), || (!self.ep0_busy.get()).then_some(())).await; + self.ep0_busy.set(true); + let answered = self.control_held(setup).await; + self.ep0_busy.set(false); + answered + } + + async fn control_held(&self, setup: Setup) -> Result, Failed>, Dead> { + if self.leaving.get() { + return Ok(Err(Failed::Gone)); + } + let length = u32::from(setup.length.min(hc::PAGE as u16)); + let at = self.hc().dma.device_addr(self.data); + // Setup (§6.4.1.2.1): its Transfer Type says which data stage follows. + let (trt, dir) = match (length, setup.data_in()) { + (0, _) => (0, 0), + (_, true) => (3 << 16, hc::TRB_DIR_IN), + (_, false) => (2 << 16, 0), + }; + let mut trbs = alloc::vec![Trb { param: setup.packet(), status: 8, control: hc::TRB_SETUP | hc::TRB_IDT | trt }]; + if length > 0 { + trbs.push(Trb { param: at, status: length, control: hc::TRB_DATA | hc::TRB_ISP | dir }); + } + // The status stage goes the other way from the data, and IN when + // there is none (§8.5.3). + let status_dir = if length > 0 && setup.data_in() { 0 } else { hc::TRB_DIR_IN }; + trbs.push(Trb { param: 0, status: 0, control: hc::TRB_STATUS | hc::TRB_IOC | status_dir }); + let mut op = { + let mut ring = self.ep0.borrow_mut(); + self.hc().transfer(self.slot, 1, &mut ring, &trbs, true) + }; + let deadline = self.wake().now() + CONTROL_NS; + let done = match op.answer(self.wake(), deadline).await { + Some(done) => done, + None => { + return Ok(match self.restart(1, Some(op)).await? { + Ok(()) => Err(Failed::Silent), + Err(()) => Err(Failed::Stuck), + }) + } + }; + Ok(match done { + Done::Gone => Err(Failed::Gone), + Done::Event { code: hc::CC_SUCCESS | hc::CC_SHORT_PACKET, moved, .. } => { + let moved = moved.min(length) as usize; + Ok((0..moved).map(|i| self.read8(self.data + i)).collect()) + } + Done::Event { code, .. } => match self.restart(1, None).await? { + Ok(()) if code == hc::CC_STALL => Err(Failed::Stall), + Ok(()) => Err(Failed::Code(code)), + Err(()) => Err(Failed::Stuck), + }, + }) + } + + /// A byte of the grant. + pub fn read8(&self, at: usize) -> u8 { + let word = self.hc().dma.read32(at & !3); + (word >> ((at & 3) * 8)) as u8 + } + + /// Bytes into the grant, a dword at a time from a dword-aligned `at`. + pub fn write_bytes(&self, at: usize, bytes: &[u8]) { + for (i, chunk) in bytes.chunks(4).enumerate() { + let mut word = [0u8; 4]; + word[..chunk.len()].copy_from_slice(chunk); + self.hc().dma.write32(at + i * 4, u32::from_le_bytes(word)); + } + } + + /// The state the controller's output context gives endpoint `dci`. + pub fn endpoint_state(&self, dci: u8) -> EndpointState { + EndpointState::decode(self.hc().dma.read32(self.hc().context(self.out, usize::from(dci)))) + } + + /// Put `trbs` on endpoint `dci`'s ring. + pub fn submit(&self, dci: u8, trbs: &[Trb]) -> Op<'u, R, D> { + let mut rings = self.rings.borrow_mut(); + let ring = rings.get_mut(&dci).expect("a configured endpoint's ring"); + self.hc().transfer(self.slot, dci, ring, trbs, false) + } + + /// Endpoint `dci` back to Running from whatever state its controller + /// gives it (`recovery`), taking back `held`, a transfer it was given and + /// never answered: once Set TR Dequeue Pointer has moved the ring past it, + /// the controller holds none of it. `Err` for an endpoint that would not + /// run again. + pub async fn restart(&self, dci: u8, held: Option>) -> Result, Dead> { + let state = self.endpoint_state(dci); + let (mut recovery, mut act) = match Recovery::begin(state) { + Ok(begun) => begun, + Err(why) => { + self.usb.say(format_args!("slot {} endpoint {dci}: {why:?}", self.slot)); + if let Some(op) = held { + op.reclaimed(); + } + return Ok(Err(())); + } + }; + loop { + match act { + Act::Command(command) => { + let id = u32::from(self.slot) << 24 | u32::from(dci) << 16; + let trb = match command { + Command::ResetEndpoint => Trb { control: hc::TRB_RESET_ENDPOINT | id, ..Trb::default() }, + Command::StopEndpoint => Trb { control: hc::TRB_STOP_ENDPOINT | id, ..Trb::default() }, + Command::SetDequeue => { + let rings = self.rings.borrow(); + let ring = if dci == 1 { *self.ep0.borrow() } else { *rings.get(&dci).expect("configured") }; + Trb { param: ring.dequeue(&self.hc().dma), status: 0, control: hc::TRB_SET_TR_DEQUEUE | id } + } + }; + let done = self.hc().command(self.wake(), trb).await?; + if !done.succeeded() { + self.usb.say(format_args!( + "slot {} endpoint {dci}: {} from {state}: {done:?}", + self.slot, + command.name() + )); + if let Some(op) = held { + op.reclaimed(); + } + return Ok(Err(())); + } + } + // A control endpoint's stall is cleared by the next Setup + // (USB 2.0 §8.5.3.4); any other is cleared at the device too. + Act::ClearHalt if dci == 1 => {} + Act::ClearHalt => { + let address = (dci / 2) | if dci % 2 == 1 { 0x80 } else { 0 }; + let clear = Setup { request_type: 0x02, request: 1, value: 0, index: u16::from(address), length: 0 }; + // Boxed: a control transfer's own recovery comes back here. + if Box::pin(self.control(clear)).await?.is_err() { + // After Set TR Dequeue Pointer (`Recovery`'s order), so + // the ring is already past `held`. + if let Some(op) = held { + op.reclaimed(); + } + return Ok(Err(())); + } + } + Act::Running => break, + } + act = recovery.completed(); + } + if let Some(op) = held { + op.reclaimed(); + } + Ok(Ok(())) + } +} + +/// What an interface is bound as. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Function { + Hid, + Disk, +} diff --git a/lib/xhci/src/xecp.rs b/system/usbd/xhci/src/xecp.rs similarity index 100% rename from lib/xhci/src/xecp.rs rename to system/usbd/xhci/src/xecp.rs diff --git a/tests/checks.rs b/tests/checks.rs index cd9597982a1..2812a6cb1a7 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -24,8 +24,6 @@ mod checks { mod screen_checks; #[path = "serial.rs"] mod serial_checks; - #[path = "usb.rs"] - mod usb_checks; /// One subject: what a console line says died, what a wait does about it, /// and that only one place in the harness answers either. @@ -520,7 +518,7 @@ mod checks { let opening = [report((100, 100), read_back), exit("pwd")].concat(); let own = [report((3_000_000, 1_100_000), (700_000, 600_000)), exit(WINDOWS_LOAD)].concat(); let good = [hold.as_str(), &first, &opening, &own].concat(); - let exited = windows_load_exited(); + let exited = exited(WINDOWS_LOAD); mask_windows(&good, 2).map_err(|e| format!("the good capture was refused: {e}"))?; let unpaired = |what: &str, capture: &str, says: &str| match mask_windows(capture, 2) { @@ -584,9 +582,9 @@ mod checks { #[test] fn irq_census_verdict() -> Result<(), String> { let census = |at: &str, on: u32, cpu: u32, kick: u64, tlb: u64| { - let xhci = if cpu == 0 { 40 } else { 0 }; + let userdev = if cpu == 0 { 40 } else { 0 }; format!( - "[ {at} cpu{on} kernel] irq: cpu{cpu} timer=100 kick={kick} xhci={xhci} userdev=0 \ + "[ {at} cpu{on} kernel] irq: cpu{cpu} timer=100 kick={kick} userdev={userdev} \ dmafault=0 tlb={tlb} nmi=0 spurious=0 unclaimed=0\n" ) }; @@ -621,7 +619,7 @@ mod checks { }; refused( "an AP's device delivery on a line that is not that AP's last", - &good.replace(&y_cpu2, &y_cpu2.replace("xhci=0", "xhci=1")), + &good.replace(&y_cpu2, &y_cpu2.replace("userdev=0", "userdev=1")), "addressed to physical destination 0", )?; refused( @@ -941,11 +939,6 @@ mod checks { audio_checks::judges_verdict() } - #[test] - fn metal_usb_judge() -> Result<(), String> { - usb_checks::transport_break_verdict() - } - #[test] fn metal_claim_spends_one_remapping_entry() { claims_checks::one_entry_per_slot(); @@ -957,8 +950,8 @@ mod checks { } #[test] - fn metal_stop_owes_its_record_and_leaves_no_operation_open() { - metal_checks::the_stop_owes_its_record_and_leaves_no_operation_open(); + fn metal_stop_owes_its_record() { + metal_checks::the_stop_owes_its_record(); } #[test] @@ -1106,7 +1099,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 dmafault=0 tlb=0 \ + let census = "| irq: cpu0 timer=9 kick=2 userdev=1729 dmafault=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\ @@ -1162,12 +1155,18 @@ mod checks { let unnamed = format!("{staged}{}{}| cpu0 pc=0xffff80006051b0b2 {spin}\n", awake(1), awake(0)); assert!(judge(&[&readback("deadlinewedge", &wedged(&unnamed), kernel)]).is_err()); - let sweep = "| [ 1.526 cpu0 kernel] usb-load: sweeping disk 0 from block 6569336 to 7507812, \ - rewriting each run with the bytes just read from it, until this machine is \ - reset out from under it\n"; - let judge = metal_judge("usb_reset_records_the_phase_it_cut"); - assert_eq!(judge(&[&readback("usbload", &wedged(sweep), kernel)]), Ok(())); + // The stick load's boot: its deadline ended it while the job's lines + // were reaching /log; one whose job never wrote, or that handed the + // machine back, is no reset under load. + let judge = metal_judge("stick_survives_a_reset_under_load"); + let line = |n: u32| format!("[2026-09-29 10:33:40 20.100 test-runner pid=12] stick_load: line {n} {}\n", "x".repeat(840)); + let wrote = format!("{kernel}{}", (0..1200).map(line).collect::()); + let short = format!("{kernel}{}", (0..10).map(line).collect::()); + assert!(judge(&[&readback("usbload", &wedged(""), &short)]).is_err(), "ten lines are no mebibyte"); + assert_eq!(judge(&[&readback("usbload", &wedged(""), &wrote)]), Ok(())); assert!(judge(&[&readback("usbload", &wedged(""), kernel)]).is_err()); + let handed_back = format!("{}| log-tail: [20.200 cpu0 kernel] Rebooting.\n", wedged("")); + assert!(judge(&[&readback("usbload", &handed_back, &wrote)]).is_err()); } /// The foreign-identity arm's pass after the reset: the record it cleared @@ -1201,40 +1200,6 @@ mod checks { assert!(judge(&[&readback("foreignrecord", &unbounded, kernel)]).is_err()); } - /// A T14 controller's handoff: it publishes USB Legacy Support and - /// firmware never claimed it. The T14 has two, and each is judged. - #[test] - fn the_xecp_judge_reads_the_t14s_handoff() { - let t14 = "[2026-09-29 11:05:25 0.253 cpu0 kernel] xHCI: xecp selftest 8/8 malformed lists refused\n\ - [2026-09-29 11:05:25 0.253 cpu0 kernel] xHCI: firmware did not claim the controller \ - (USBLEGSUP 0x01002201)\n\ - [2026-09-29 11:05:25 0.253 cpu0 kernel] xHCI: USBLEGCTLSTS 0xe0000000 -> 0x00000000 \ - (SMI generation off)\n\ - [2026-09-29 11:05:25 0.253 cpu0 kernel] xHCI: controller reset\n\ - [2026-09-29 11:05:25 0.254 cpu0 kernel] xHCI: controller started\n"; - assert_eq!(xhci_xecp(t14), Ok(())); - assert_eq!(xhci_xecp(&format!("{t14}{t14}")), Ok(())); - // Firmware that kept the controller handed nothing over. - let held = t14.replace( - "firmware did not claim the controller (USBLEGSUP 0x01002201)", - "firmware still owns the controller after 1000ms (USBLEGSUP 0x01010001 -> \ - 0x01010001) — resetting it anyway", - ); - assert!(xhci_xecp(&held).is_err()); - assert!(xhci_xecp(&format!("{t14}{held}")).is_err()); - let kept = held.replace("xHCI: controller reset", "xHCI: 34 scratchpad buffers configured"); - assert!(xhci_xecp(&format!("{t14}{kept}")).is_err()); - // A second controller's handoff with no reset of its own, and a reset - // with no handoff before it. - let unreset = t14.replace("xHCI: controller reset", "xHCI: 34 scratchpad buffers configured"); - assert!(xhci_xecp(&format!("{t14}{unreset}")).is_err()); - assert!(xhci_xecp(&format!("{unreset}{t14}")).is_err()); - let unhanded = t14.replace("firmware did not claim the controller", "USB 3.1 on ports 2..=5"); - assert!(xhci_xecp(&format!("{t14}{unhanded}")).is_err()); - let silent = unhanded.replace("xHCI: controller reset", "xHCI: 34 scratchpad buffers configured"); - assert!(xhci_xecp(&silent).is_err()); - } - /// A member that reads another test binary by path. #[test] fn a_shared_chunk_stages_every_binary_its_members_name() { diff --git a/tests/checks/metal.rs b/tests/checks/metal.rs index 5efe5843b6d..5cc10ce254f 100644 --- a/tests/checks/metal.rs +++ b/tests/checks/metal.rs @@ -146,18 +146,15 @@ fn read(dir: &Path, labels: &[&str]) -> BTreeMap Result<(), String> { - let judged = |what: &str, log: &str, green: bool| { - match (usb::transport_break_recovered(&Serial::named(what, log)), green) { - (Ok(()), true) | (Err(_), false) => Ok(()), - (Ok(()), false) => Err(format!("{what} passed a log it has to refuse:\n{log}")), - (Err(why), true) => Err(format!("{what} refused a log it has to pass: {why}\n{log}")), - } - }; - const STAGED: &str = "\ - [ 1.174 cpu3 kernel] usb-storage: 00:14.0 slot 1 transport broke on SCSI 0x2a: a staged \ - break skipped the data phase wait; break 1 of 3 running\n"; - const OWED: &str = "\ - [ 1.174 cpu3 kernel] usb-storage: 00:14.0 slot 1 is owed the data of the command that \ - broke, so nothing can be asked of it on the Bulk-Out: its port is reset with no class \ - reset before it\n"; - const RESET: &str = "\ - [ 1.229 cpu3 kernel] xHCI: 00:14.0 slot 1 port 1 reset while recovering (hot on a USB2 \ - port): PORTSC 0x00000e03 then 0x00200e03, link Active, speed 3: reset, and the port is \ - enabled\n"; - const UNANSWERED: &str = "\ - [ 1.279 cpu3 kernel] xHCI: Address Device (after the port reset) failed: code 4 (USB \ - Transaction Error)\n"; - const CLIMBED_ON: &str = "\ - [ 1.279 cpu3 kernel] usb-storage: 00:14.0 slot 1 the port reset was not answered; break 2 \ - of 3 running\n\ - [ 1.279 cpu3 kernel] usb-storage: 00:14.0 slot 1 broke 2 times running; its port reset did \ - not bring the transport back\n\ - [ 1.279 cpu3 kernel] xHCI: 00:14.0 slot 1 port 1 reset while taking it offline (hot on a \ - USB2 port): PORTSC 0x000202a0 then 0x000202a0, link RxDetect, speed 0: nothing is \ - connected, so its port's teardown takes it from here\n\ - [ 1.279 cpu3 kernel] usb-storage: 00:14.0 slot 1 is offline: both bulk endpoints \ - Stopped=true, port 1 reset=false and nothing sent after it, Reset Device=false, its slot \ - goes back to the controller; every operation on it is refused from here\n"; - const LEFT: &str = "\ - [ 1.279 cpu3 kernel] usb-storage: 00:14.0 slot 1 the port reset was not answered, and port \ - 1 no longer holds the device (PORTSC 0x000202a0): its port's teardown takes it from here\n"; - const BACK: &str = "\ - [ 1.279 cpu3 kernel] usb-storage: disk 0 left port 1 (its port read empty) after this \ - driver reset it; it is held 1894 ms for the same device to come back\n\ - [ 2.211 cpu0 kernel] usb-storage: disk 0 came back on port 13 slot 6 as the same device \ - (USB 0781:5581, serial number \"FEDCBA98765432FEDCBA\", 7507812 blocks of 512 B), \ - msc_block +0x30000; its volume carries on\n\ - [ 2.259 cpu3 kernel] usb-storage: disk 0 is back, and the operation it was asked went out \ - again on it: it completed\n"; - const TOOK: &str = "\ - [ 1.330 cpu3 kernel] usb-storage: 00:14.0 slot 1 the port reset took: addressed and \ - configured again, the device answered TEST UNIT READY under its own tag 0x5a2\n"; - const COMPLETED: &str = "\ - [ 1.331 cpu3 kernel] usb-storage: 00:14.0 slot 1 SCSI 0x2a completed after 1 break(s) \ - running; the transport came back and the count is cleared\n"; - const CLASS_RESET_TOOK: &str = "\ - [ 1.175 cpu3 kernel] usb-storage: 00:14.0 slot 1 Reset Recovery took: the device answered \ - TEST UNIT READY under its own tag 0x5a2\n"; - const CLASS_RESET_UNANSWERED: &str = "\ - [ 1.175 cpu3 kernel] usb-storage: 00:14.0 slot 1 the class reset was not answered; break 2 \ - of 3 running\n"; - - judged("the break as the T14 read it", &format!("{STAGED}{OWED}{RESET}{UNANSWERED}{CLIMBED_ON}{BACK}"), false)?; - judged("the break its stick left", &format!("{STAGED}{OWED}{RESET}{UNANSWERED}{LEFT}{BACK}"), true)?; - let lost = BACK.replace( - "came back on port 13 slot 6 as the same device", - "did not come back within 2000 ms of its port reset; it is offline", - ); - judged("a stick that never came back", &format!("{STAGED}{OWED}{RESET}{UNANSWERED}{LEFT}{lost}"), false)?; - let failed = BACK.replace("it completed", "it failed"); - judged( - "a write that failed on the stick that came back", - &format!("{STAGED}{OWED}{RESET}{UNANSWERED}{LEFT}{failed}"), - false, - )?; - judged("a stick that answered on its port", &format!("{STAGED}{OWED}{RESET}{TOOK}{COMPLETED}"), true)?; - judged( - "a stick owed a write's data that the class reset brought back", - &format!("{STAGED}{CLASS_RESET_TOOK}{COMPLETED}"), - false, - )?; - judged( - "a stick owed a write's data given the class reset before its port reset moved it", - &format!("{STAGED}{CLASS_RESET_UNANSWERED}{RESET}{UNANSWERED}{LEFT}{BACK}"), - false, - )?; - Ok(()) -} diff --git a/tests/common/audio.rs b/tests/common/audio.rs index ffd381c4033..b677233e7d4 100644 --- a/tests/common/audio.rs +++ b/tests/common/audio.rs @@ -83,6 +83,32 @@ pub fn tone_on_metal(log: &Serial) -> Result<(), String> { Ok(()) } +/// The job that plays `hda_tone`'s tone while it writes the stick. +pub const TONE_BESIDE_STICK_WRITE: &str = "test_rs_tone_beside_stick_write"; + +/// That job's tone played to its end through the HDA controller with no +/// period left to soundserver, while at least a mebibyte of its lines reached +/// the stick: counted in the file read back off it, not in what the job says. +pub fn tone_beside_a_stick_write(log: &Serial) -> Result<(), String> { + on_an_output(log.text(), TONE_BESIDE_STICK_WRITE)?; + let window = job_window(log.text(), TONE_BESIDE_STICK_WRITE)?; + let reached = super::metal::bytes_of_lines(window, "tone_beside_stick_write: line "); + if reached < 1 << 20 { + return Err(format!("{reached} bytes of the job's lines reached the stick beside the tone, under a mebibyte")); + } + let wrote = format!("{} KiB of lines reached the stick beside the tone", reached >> 10); + let underruns = sum_field(window, "underruns"); + if underruns != 0 { + return Err(format!( + "soundserver filled {underruns} period(s) the tone client had not covered, beside a stick \ + write ({}):\n{window}", + wrote.trim() + )); + } + eprintln!(" [audio] {}", wrote.trim()); + Ok(()) +} + /// The resampled tone client played to its end through the HDA controller, at /// a rate the device does not run, and no period of it went unfilled. pub fn resampled_tone_on_metal(log: &Serial) -> Result<(), String> { diff --git a/tests/common/claims.rs b/tests/common/claims.rs index c4f83fd457c..bcdfdbdb79d 100644 --- a/tests/common/claims.rs +++ b/tests/common/claims.rs @@ -24,10 +24,6 @@ pub const NOT_REMAPPED: &str = "its interrupts would not be remapped on this mac /// The kernel's reason where the units remap and one filters no SMI. pub const SMI_UNFILTERED: &str = "an IOMMU unit on this machine filters no SMI"; -/// The kernel's line where firmware's DMAR flags leave interrupt remapping off, -/// which `iommu-no-remap` stands in for: the premise of the refusal. -const INTR_REMAP_CLEAR: &str = "leave INTR_REMAP clear, so firmware says this platform does not remap"; - fn records(log: &Serial) -> impl Iterator { log.text().lines().filter_map(bootlog::message) } @@ -164,17 +160,6 @@ pub fn clear_of_host_bridges(log: &Serial) -> Result<(), String> { Ok(()) } -/// On a machine whose units do not remap, the I219's claim is refused by that -/// reason before anything on the function is armed, and the boot completes. -pub fn refused_unremapped(log: &Serial) -> Result<(), String> { - log.must_say(INTR_REMAP_CLEAR)?; - let at = function(log)?; - log.must_not_say(&format!("[{I219}] handed over"))?; - refused_at(log, &at, NOT_REMAPPED)?; - log.must_say(bootlog::COMPLETE)?; - Ok(()) -} - /// The claim of the function at `at` refused for `why`, before anything on /// it changed: no message armed and no entry written. pub fn refused_at(log: &Serial, at: &str, why: &str) -> Result<(), String> { diff --git a/tests/common/devices.rs b/tests/common/devices.rs index 61ba69a13c7..f333a53d05e 100644 --- a/tests/common/devices.rs +++ b/tests/common/devices.rs @@ -7,7 +7,6 @@ pub const JOBS: &[&str] = &["usbwrite", "usbread", "fbcheck", "fbfill", "fbread"]; pub const CONFIG: &str = "tests/images/metaldevicecase.toml"; -pub const BOOT: &str = "metaldevicecase"; /// The runner's job list is the one this file names, under the symlinks that /// give each job the name the kernel's `exit:` record carries. diff --git a/tests/common/iommu.rs b/tests/common/iommu.rs index d3902bd7cc0..92fe9589389 100644 --- a/tests/common/iommu.rs +++ b/tests/common/iommu.rs @@ -155,6 +155,10 @@ pub fn iommu_virtio_platform() -> Result<(), String> { /// the one its nvme row drives. const NVME_AT: &str = "00:02.0"; +/// The slot QEMU's `-device` order puts the boot controller on, the one the +/// image's usbd row drives. +const XHCI_AT: &str = "00:01.0"; + /// The slot QEMU's `-device` order puts the virtio-sound function on, the one /// `tests/images/netcase.toml`'s soundserver row claims. const SOUND_AT: &str = "00:04.0"; @@ -180,7 +184,11 @@ fn not_handed_over(at: &str, why: &str) -> String { /// would fail. The NVMe controller and the virtio-sound function are refused /// the same. fn no_unit_is_no_claim(log: &Serial) -> Result<(), String> { - refused_claim(log, NETSTACK_CLAIMS, NOT_REMAPPED, &[NVME_AT, SOUND_AT])?; + refused_claim(log, NETSTACK_CLAIMS, NOT_REMAPPED, &[XHCI_AT, NVME_AT, SOUND_AT])?; + log.must_say(¬_handed_over(XHCI_AT, NOT_REMAPPED))?; + log.must_say(&format!( + "supervisor: usbd was not started on PCI {XHCI_AT}: pci:0c:03:30/1033:0194 is on this machine and could not be handed over" + ))?; log.must_say(¬_handed_over(SOUND_AT, NOT_REMAPPED))?; log.must_say("supervisor: soundserver: pci:04:01:00/1af4:1059 is on this machine and could not be handed over")?; log.must_say(¬_handed_over(NVME_AT, NOT_REMAPPED))?; @@ -305,7 +313,7 @@ pub(crate) fn unit_fields(line: &str) -> BTreeMap { } /// Every function `pci::enumerate` printed. Anchored on the class field that -/// follows the address, so `xHCI: found at PCI 00:02.0` is not one of them. +/// follows the address, so `pcidev: PCI 00:02.0 [...]` is not one of them. fn enumerated_functions(log: &Serial) -> BTreeSet { log.text() .lines() @@ -325,12 +333,10 @@ const AMDVI_SERVES_XHCI: &str = "iommu: amdvi unit0 serves 00:03.0 data=0x00"; const AMDVI_OFF: &str = "iommu: amdvi unit0 @0xfed80000 switched off control=0x0000000000000000"; /// The unit programmed and on. const AMDVI_TRANSLATING: &str = "iommu: amdvi unit0 @0xfed80000 translating control="; -/// The boot stick's controller on the first domain a driver made, the one -/// past the identity domain. -const XHCI_MOVED: &str = "iommu: 00:03.0 moves to domain2"; -/// The USB gate: the boot stick bound through the controller behind the unit, -/// which only DMA the unit lets through can do. -const STICK_BOUND: &str = "usb-storage: 1 device(s)"; +/// The boot stick's controller refused to a claim: this unit remaps no +/// message, so no function behind it is handed to a process, and the +/// controller's DMA is no question this machine can answer. +const XHCI_REFUSED: &str = "pcidev: PCI 00:03.0 NOT HANDED OVER — its interrupts would not be remapped on this machine"; /// What the actuator leaves on an AMD-Vi unit, in the kernel's words for /// `unit`: one line per field the hand-over switches off by its own write. @@ -339,20 +345,19 @@ pub(crate) fn amdvi_left_on(unit: &str) -> [String; 3] { .map(|field| format!("iommu: amdvi {unit} was handed over with {field} on; it goes off")) } -/// **A unit an AMD machine's firmware left on is switched off and programmed, -/// and the boot stick's DMA reaches memory through its controller's own -/// domain.** QEMU's `amd-iommu` is the only AMD-Vi unit -/// the nightly guest suite runs: the T14 is an Intel machine, and the hand-over's -/// register sequence and the DMA it frees are neither of them a host test's. -/// Two boots: the shipped kernel finds a unit nothing left on and switches -/// nothing off; the test kernel's `iommu-firmware-left` leaves the unit blocking -/// every device's DMA with its command buffer and event log running, and -/// each goes off by its own write before the stick is driven. With the -/// switch-off reverted that boot reds on the fields' lines and on `control` -/// reading back zero, and its stick still binds: QEMU 11.1.1 moves no -/// function to translation on `INVALIDATE_IOMMU_ALL` -/// (`issues/qemus-invalidate-iommu-all-leaves-every-function-untranslated.md`), -/// so the AMD bench's `iommu_firmware_left` row holds the DMA half. +/// **A unit an AMD machine's firmware left on is switched off and +/// programmed.** QEMU's `amd-iommu` is the only AMD-Vi unit the nightly guest +/// suite runs: the T14 is an Intel machine, and the hand-over's register +/// sequence is no host test's. Two boots: the shipped kernel finds a unit +/// nothing left on and switches nothing off; the test kernel's +/// `iommu-firmware-left` leaves the unit blocking every device's DMA with its +/// command buffer and event log running, and each goes off by its own write. +/// With the switch-off reverted that boot reds on the fields' lines and on +/// `control` reading back zero. **The DMA half is the AMD bench's +/// `iommu_firmware_left` row's**: the kernel drives no function behind this +/// unit, and this one remaps no message, so the boot stick's controller is +/// refused to usbd here; QEMU's unit that remaps filters no SMI, which is +/// refused too (`usbd_refused_without_smi_filter`). pub fn amdvi_firmware_left(test_config: &Path) -> Result<(), String> { let profile = Profile::MetalAmdVi; let argv = qemu::profile_argv(&BootOptions { profile, ..Default::default() }); @@ -367,14 +372,13 @@ pub fn amdvi_firmware_left(test_config: &Path) -> Result<(), String> { let log = Serial::boot(&qemu); log.must_be_clean()?; log.must_say("Boot: complete")?; - log.must_say(STICK_BOUND)?; + log.must_say(XHCI_REFUSED)?; log.must_say("iommu: IVRS rev=1 ivinfo=0x00002801 efr_images=y preboot_dma_protection=n pa=40 va=0")?; log.must_say(AMDVI_UNIT)?; log.must_say(AMDVI_SUPERSEDED)?; log.must_say(AMDVI_SERVES_XHCI)?; log.must_say(AMDVI_OFF)?; log.must_say(AMDVI_TRANSLATING)?; - log.must_say(XHCI_MOVED)?; log.must_not_say("isolates no device")?; log.must_not_say("no DMAR or IVRS table")?; let handed = log.must_say("iommu: amdvi unit0 handed over control=")?; @@ -392,7 +396,7 @@ pub fn amdvi_firmware_left(test_config: &Path) -> Result<(), String> { } else { log.must_not_say("was handed over with")?; } - eprintln!(" [amdvi] left on = {left}: {handed}; the stick bound behind the unit"); + eprintln!(" [amdvi] left on = {left}: {handed}; the stick's controller refused to a claim behind it"); } Ok(()) } diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs index 48a85998a3d..de38f7db579 100644 --- a/tests/common/irqcensus.rs +++ b/tests/common/irqcensus.rs @@ -15,15 +15,14 @@ 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; 9] = - ["timer", "kick", "xhci", "userdev", "dmafault", "tlb", "nmi", "spurious", "unclaimed"]; +pub const SOURCES: [&str; 8] = ["timer", "kick", "userdev", "dmafault", "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; 3] = ["xhci", "userdev", "dmafault"]; +pub const DEVICE_SOURCES: [&str; 2] = ["userdev", "dmafault"]; /// One CPU's counters out of one `irq:` line. #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/tests/common/metal.rs b/tests/common/metal.rs index 1c01c7a5284..3e98d9f547e 100644 --- a/tests/common/metal.rs +++ b/tests/common/metal.rs @@ -375,10 +375,6 @@ impl Readback { self.label, park.sweep.running, )), - Some(park) if park.in_flight != 0 => Err(format!( - "{}'s stop ended with {} block operation(s) open:\n {park}", - self.label, park.in_flight, - )), Some(_) => Ok(()), } } @@ -540,6 +536,21 @@ fn within_one_period(late_ms: i64, period_ns: u64) -> Result<(), String> { } /// One image, and every test that rides it. +/// The bytes of every line of `text` that carries `marker`, newline included: +/// what of a load's lines a file read back off the stick holds. +pub fn bytes_of_lines(text: &str, marker: &str) -> usize { + text.lines().filter(|l| l.contains(marker)).map(|l| l.len() + 1).sum() +} + +/// The job that writes the boot stick for ever (`stick_load.rs`), so its boot +/// is reset by its own deadline with a write in flight. +pub const STICK_LOAD_JOB: &str = "test_rs_stick_load"; + +/// That boot's deadline: past both benches' `Boot: complete` (12 s on the +/// T14) and the job's first synced mebibytes, and before the runner's list +/// bound ([`toyos_tco::JOB_BOUND_MS`]). +pub const STICK_LOAD_DEADLINE_MS: u64 = 40_000; + pub struct Batch { config: &'static str, params: Vec<&'static str>, @@ -568,7 +579,16 @@ impl Batch { } /// The bound the kernel gives this whole boot, in milliseconds. + /// + /// **A boot carrying [`STICK_LOAD_JOB`] is ended by it on purpose**, with + /// the job still writing the stick: [`STICK_LOAD_DEADLINE_MS`], below the + /// list's own bound, so the kernel's reset path, which touches no USB + /// register, lands before the runner would hand the machine back. pub fn deadline_ms(&self) -> u64 { + if self.jobs.iter().any(|job| job == STICK_LOAD_JOB) { + assert!(STICK_LOAD_DEADLINE_MS < self.bound_ms(), "the load's deadline is past its runner's bound"); + return STICK_LOAD_DEADLINE_MS; + } toyos_metal::bound_for(&self.params, self.bound_ms()) } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 3a2078e114f..de03db65db1 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -21,6 +21,5 @@ pub mod ps2server; pub mod qemu; pub mod screen; pub mod serial; -pub mod usb; /// A VNC client, as far as resizing a host window takes one. pub mod vnc; diff --git a/tests/common/power.rs b/tests/common/power.rs index dc5cb437d05..27c7e24e7d7 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -364,9 +364,6 @@ pub fn done_chain(after: &serial::Serial) -> Result<(), String> { // The chain ends rather than going round: a pass that booted a kernel would // have said so, and this one must not have. says_nothing_of(after, bootlog::LOADER_LAST_LINE)?; - // What its transport went through is on every record, in one line where - // nothing broke. - after.must_say_after(bootlog::HANDED_BACK, toyos_blackbox::RECOVERY_OPENS_WITH)?; after.must_say(bootlog::CHAIN_ENDS_LINE)?; eprintln!(" [power] a deliberate reboot sealed DONE and the chain ended in a reset"); Ok(()) @@ -406,49 +403,6 @@ fn death_took_the_census(after: &serial::Serial) -> Result<(), String> { Ok(()) } -/// The metal half of the load arm: a T14 boot that never stopped writing, ended -/// by the boot deadline with its controller mid-transfer, and the stick still -/// there afterwards. -/// -/// **The bound is the boot deadline's and not the hard-lockup detector's.** The -/// sweep keeps its CPU taking interrupts for exactly that reason -/// (`usb_gate::sweep_under_load`): a CPU that takes none is ended half a bound -/// earlier by `kernel/src/hardlockup`, which is a different mechanism reported -/// under this arm's name, so this judge names the bound it demands. -pub fn usb_load_chain(after: &serial::Serial) -> Result<(), String> { - after.must_say(bootlog::PREVIOUS_PANIC)?; - death_took_the_census(after)?; - after.must_say_after(bootlog::PREVIOUS_PANIC, bootlog::DEADLINE_EXPIRED)?; - // The sweep starts inside the stop, after the supervisor had the file made whole, so - // its records cross only in the page's tail. - after.must_say_after(bootlog::PREVIOUS_PANIC, bootlog::USB_LOAD_RUNNING)?; - // A sweep that refused, one the disk stopped answering, and one that swept - // its whole span before the reset: each is a boot that measured the idle - // case again under this arm's name. - says_nothing_of(after, bootlog::USB_LOAD_REFUSED)?; - says_nothing_of(after, bootlog::USB_LOAD_STOPPED)?; - says_nothing_of(after, bootlog::USB_LOAD_SWEPT)?; - says_nothing_of(after, REBOOTING)?; - // A page that names the other bound is this arm measuring a hard lockup. - says_nothing_of(after, bootlog::LOCKED_UP)?; - // **The account reaching the page is itself under test**: it is made a line - // at a time from the reset path, under the reserve - // `toyos_blackbox::ACCOUNT_BYTES` keeps for it. - after.must_say(toyos_metal::devices::QUIESCE_HEAD)?; - after.must_say(bootlog::CHAIN_ENDS_LINE)?; - // **Reported and not judged.** Which state the reset found the controller - // in is the open question this arm gathers answers to; a predicate over it - // would be the suite deciding it. A reset that found no command open writes - // no such line, and that is a fact about the boot rather than a failure of - // it. - let endpoint = toyos_metal::devices::QUIESCE_ENDPOINT; - match after.text().lines().find(|line| line.contains(endpoint)) { - Some(said) => eprintln!(" [power] {}", said.trim()), - None => eprintln!(" [power] the reset found no command open on the device"), - } - Ok(()) -} - /// The `cpuN` the bracket of `line`'s record names, before `needle`. fn record_cpu<'a>(line: &'a str, needle: &str) -> Option<&'a str> { line.split(needle) @@ -607,46 +561,26 @@ pub fn blackbox_unclaimed( Ok(()) } -const TOOK_THE_LOCK: &str = "the controller lock was held from before the log volume's"; - -/// The T14's judge for [`usb_reset_hands_devices_back`]. -/// -/// **The machine is the judge of the device, and nothing else is.** QEMU cannot -/// wedge a stick. What is left for this to read is the reset's own account, -/// which on this machine is in `loader.log`'s pass after the reset rather than -/// in any file the kernel wrote. -/// -/// Both arms are orderly reboots, so both owe the barrier. -pub fn usb_reset_on_metal(arms: &[&super::metal::Readback]) -> Result<(), String> { - let mut bad = Vec::new(); - for back in arms { - let text = back.loader(); - let head = toyos_metal::devices::QUIESCE_HEAD; - let Some(account) = toyos_metal::devices::quiesced(text.text()) else { - bad.push(format!( - "{}: loader.log carries no readable {head:?} summary, so nothing stopped this \ - machine's USB before it reset", - back.label - )); - continue; - }; - if !account.complete() { - bad.push(format!("{}: {account:?}", back.label)); - } - if !text.text().contains(TOOK_THE_LOCK) { - bad.push(format!( - "{}: this is an orderly reboot and its account does not say {TOOK_THE_LOCK:?}", - back.label - )); - } - eprintln!(" [power] {}: {account:?}", back.label); - } - if bad.is_empty() { - return Ok(()); - } - Err(format!("{} finding(s):\n {}", bad.len(), bad.join("\n "))) -} - /// What `/system/bin/acpiserver` says once it serves q35's power button, /// whole, before it loads the tables. const ACPI_ARMED: &str = "acpiserver: armed: power button served"; + +/// [`STICK_LOAD_JOB`](super::metal::STICK_LOAD_JOB)'s boot: the job wrote the +/// stick through usbd, the boot deadline ended the machine while it did, and +/// the stick came back on the next host. How long it took to enumerate is +/// judged against the bench's record (`boot.usbload.stick_secs`), and a stick +/// that never came back is a readback that failed before this was asked. +pub fn stick_survives_a_reset_under_load(back: &super::metal::Readback) -> Result<(), String> { + let after = back.after_the_reset()?; + after.must_say(bootlog::PREVIOUS_PANIC)?; + after.must_say_after(bootlog::PREVIOUS_PANIC, bootlog::DEADLINE_EXPIRED)?; + says_nothing_of(&after, REBOOTING)?; + // What reached the medium: the load's lines in the file read back off the + // stick, which logkeeper wrote through usbd. + let reached = super::metal::bytes_of_lines(back.log().text(), "stick_load: line "); + if reached < 1 << 20 { + return Err(format!("{reached} bytes of the load's lines reached the stick, under a mebibyte")); + } + eprintln!(" [power] {} KiB of the load's lines reached the stick before the reset", reached >> 10); + back.measured("boot.usbload.stick_secs", back.stick_secs) +} diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index cd962af2884..5552b48ff60 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -686,9 +686,9 @@ pub enum Profile { /// its claim. HeadlessVirtioGpu, /// [`Profile::Headless`] with a second controller at [`SPARE_AT`], QEMU's - /// `qemu-xhci` on MSI and with no MSI-X, carrying a stick and a keyboard: - /// the controller `xhci-leave=00:10.0` leaves to `usbd`, armed on MSI as - /// the T14's two are. + /// default `qemu-xhci`, its MSI-X table in the BAR that holds its + /// registers, carrying a stick and a keyboard: the controller no image's + /// `drives` row names, which `usbd_spare` claims. HeadlessUsbSpare, /// [`Profile::Headless`] with its virtio NIC behind a `pcie-root-port`: /// a function a claim may take whose BAR has to land inside the port's @@ -718,6 +718,10 @@ pub enum Profile { /// first with no codec and the second with an output codec: a machine /// whose drivers do not both reach an output, as the AMD bench's do not. MetalTwoHda, + /// [`Profile::Metal`] with a `usb-kbd` beside the stick, which every key + /// QMP sends goes to ([`Shape::usb`]): it reaches the owner of input + /// through usbd. + MetalUsbKeyboard, /// [`Profile::Metal`] on a CPU whose CPUID offers no x2APIC, emulated /// whatever the host, so its local APICs run in xAPIC mode: the machine /// an AMD laptop without x2APIC is. @@ -800,6 +804,7 @@ impl Profile { | Self::HeadlessTwoEdu | Self::Metal | Self::MetalTwoHda + | Self::MetalUsbKeyboard | Self::MetalNoX2apic | Self::MetalAmdVi | Self::MetalAmdViTestdev @@ -884,11 +889,9 @@ const XHCI_DEFAULT: &str = "nec-usb-xhci,id=xhci"; /// [`Profile::HeadlessUsbSpare`]'s second controller, and the stick on it, /// whose bytes are zeros and whose writes go nowhere: what is on it is no -/// question this machine asks. `msi=on` stated, because with MSI-X off and -/// MSI left `auto` this QEMU's `qemu-xhci` offers neither, and the kernel -/// refuses the claim for it. -const XHCI_SPARE: &str = "qemu-xhci,id=spare,addr=0x10,msix=off,msi=on"; -/// Where [`XHCI_SPARE`] puts the spare, which `xhci-leave=` names. +/// question this machine asks. +const XHCI_SPARE: &str = "qemu-xhci,id=spare,addr=0x10"; +/// Where [`XHCI_SPARE`] puts the spare. pub const SPARE_AT: &str = "00:10.0"; const SPARE_STICK: &str = "driver=null-co,node-name=spare-stick,size=67108864,read-zeroes=on"; @@ -1100,6 +1103,7 @@ impl Profile { }, Self::MetalNoX2apic => Self::Metal.shape(), Self::MetalTwoHda => Shape { hda: true, ..Self::Metal.shape() }, + Self::MetalUsbKeyboard => Shape { usb: &["usb-kbd,bus=xhci.0"], ..Self::Metal.shape() }, Self::MetalAmdVi => Shape { iommu: Some(Unit::AmdVi { intremap: false }), ..Self::Metal.shape() }, Self::HeadlessUsbSpareAmdVi => { Shape { iommu: Some(Unit::AmdVi { intremap: true }), ..Self::HeadlessUsbSpare.shape() } @@ -1638,6 +1642,12 @@ impl QemuInstance { &self.boot_log } + /// The disk image this boot runs from: the stick under a stick profile. + /// What the guest wrote to it is whole once [`Self::await_exit`] answers. + pub fn boot_image(&self) -> &Path { + &self.boot_image + } + pub fn stdin_mut(&mut self) -> &mut BufWriter { &mut self.stdin } diff --git a/tests/common/usb.rs b/tests/common/usb.rs deleted file mode 100644 index f062b6d03cd..00000000000 --- a/tests/common/usb.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! The USB transport break on the T14's boot stick, judged off the stick's own -//! log: `usb_stick_left`'s metal row. - -use super::serial; - -/// The staged break on a real stick: the transfer abandoned on the boot stick's -/// first WRITE(10) is recovered, the write completes, the disk keeps its -/// number, and the boot goes on to the deliberate reboot that ends its chain. -pub fn transport_break_on_metal( - kernel: &serial::Serial, - after: &serial::Serial, -) -> Result<(), String> { - transport_break_recovered(kernel)?; - super::power::done_chain(after) -} - -/// The kernel log's half of [`transport_break_on_metal`]. -/// -/// **The ladder enters at the port reset**: the break leaves the stick owed a -/// WRITE's data, across which no class reset may be asked. The stick decides -/// the rest. It answers the rung's TEST UNIT READY on its port, and the write -/// goes out again there; or it leaves its port under the reset — a SuperSpeed -/// stick enumerated on the USB2 half of its receptacle trains on the USB3 half -/// — is held, comes back as the same device, and the write goes out again on -/// it. **Either way no rung takes it offline.** -pub fn transport_break_recovered(kernel: &serial::Serial) -> Result<(), String> { - let staged = kernel.must_say( - "transport broke on SCSI 0x2a: a staged break skipped the data phase wait; break 1 of ", - )?; - let under_test = broke_on(staged)?; - let entered = kernel.must_say_after( - staged, - &format!("usb-storage: {under_test} is owed the data of the command that broke"), - )?; - kernel.must_not_say(&format!("usb-storage: {under_test} is offline"))?; - if let Ok(left) = kernel.must_say_after(entered, " after this driver reset it; it is held ") { - let back = kernel.must_say_after(left, " as the same device (USB ")?; - kernel.must_say_after( - back, - "is back, and the operation it was asked went out again on it: it completed", - )?; - eprintln!(" [usb] {left}"); - eprintln!(" [usb] {back}"); - return Ok(()); - } - let took = kernel.must_say_after(entered, &format!("usb-storage: {under_test} the port reset took"))?; - kernel.must_say_after(took, &format!("usb-storage: {under_test} SCSI 0x2a completed after "))?; - eprintln!(" [usb] {took}"); - Ok(()) -} - -/// Which device a `usb-storage: slot transport broke …` line is about. -/// -/// **Refused rather than widened if the line stops naming one.** A count of -/// broken transports is evidence about a disk, and a machine that boots off USB -/// always has at least two: the answer to "how many times did *this* disk's -/// transport break" is not recoverable from a line that does not say which disk -/// it was, and matching every disk's line instead is how this test came to red -/// on a boot stick's own clean recovery. -fn broke_on(line: &str) -> Result<&str, String> { - line.split_once("usb-storage: ") - .and_then(|(_, rest)| rest.split_once(" transport broke")) - .map(|(who, _)| who) - .ok_or_else(|| { - format!("{line:?} does not name the device whose transport broke, so nothing can \ - count that device's breaks apart from another's") - }) -} - diff --git a/tests/guest/rust-tests/Cargo.lock b/tests/guest/rust-tests/Cargo.lock index 070dae5d7c8..38cc2547932 100644 --- a/tests/guest/rust-tests/Cargo.lock +++ b/tests/guest/rust-tests/Cargo.lock @@ -908,6 +908,10 @@ version = "0.1.0" name = "toyos-font" version = "0.2.0" +[[package]] +name = "toyos-gpt" +version = "0.1.0" + [[package]] name = "toyos-i219" version = "0.1.0" @@ -921,6 +925,7 @@ name = "toyos-inspect" version = "0.1.0" dependencies = [ "toyos-abi", + "toyos-gpt", ] [[package]] diff --git a/tests/guest/rust-tests/src/bin/end_returns_claims.rs b/tests/guest/rust-tests/src/bin/end_returns_claims.rs index cdd0777e81b..a52404f12e0 100644 --- a/tests/guest/rust-tests/src/bin/end_returns_claims.rs +++ b/tests/guest/rust-tests/src/bin/end_returns_claims.rs @@ -1,6 +1,6 @@ -//! A holder's end gives its claim back before anyone can see it end: the -//! function `xhci-leave=` leaves is handed to a child that holds it, the -//! child is killed and waited for, and the claim is asked for at once, for +//! A holder's end gives its claim back before anyone can see it end: a +//! function no `drives` row names ([`SPARES`]) is handed to a child that holds +//! it, the child is killed and waited for, and the claim is asked for at once, for //! [`ROUNDS`] rounds. Any refusal is red; how long each step took is printed //! and not judged. //! diff --git a/tests/guest/rust-tests/src/bin/endowment_denied.rs b/tests/guest/rust-tests/src/bin/endowment_denied.rs index ca301ffbad6..e8bd84c471e 100644 --- a/tests/guest/rust-tests/src/bin/endowment_denied.rs +++ b/tests/guest/rust-tests/src/bin/endowment_denied.rs @@ -287,7 +287,7 @@ fn a_right_the_capability_lacks_is_a_word() { let toothless = cap.narrowed(Rights::DUP).expect("a capability carrying less"); assert_eq!( - syscall::device_claim(toothless.as_handle(), DeviceRequest::Class(DeviceType::Keyboard)).err(), + syscall::device_claim(toothless.as_handle(), DeviceRequest::Class(DeviceType::Framebuffer)).err(), Some(SyscallError::PermissionDenied), "a capability without DEVICE minted a claim", ); @@ -318,7 +318,7 @@ fn a_right_the_capability_lacks_is_a_word() { // check: `DEVICE` and `RT` are bits nothing else carries, so this is the // same refusal the narrowed capability got. assert_eq!( - syscall::device_claim(RawHandle(1), DeviceRequest::Class(DeviceType::Keyboard)).err(), + syscall::device_claim(RawHandle(1), DeviceRequest::Class(DeviceType::Framebuffer)).err(), Some(SyscallError::PermissionDenied), "a pipe was taken as a capability by SYS_DEVICE_CLAIM", ); @@ -339,7 +339,7 @@ fn a_right_the_capability_lacks_is_a_word() { // the assertion says only that it is not the refusal. A claim is released // as soon as it is taken, because a claim moves and the boot runs other // binaries that need this one. - let with_the_bit = syscall::device_claim(cap.as_handle(), DeviceRequest::Class(DeviceType::Keyboard)); + let with_the_bit = syscall::device_claim(cap.as_handle(), DeviceRequest::Class(DeviceType::Framebuffer)); assert_ne!( with_the_bit.err(), Some(SyscallError::PermissionDenied), @@ -572,7 +572,7 @@ fn not_a_handle(role: &str) -> ! { std::io::stdout().flush().expect("flush the marker"); let answered = match role { "claim-absent" => { - format!("{:?}", syscall::device_claim(HANDLE_INVALID, DeviceRequest::Class(DeviceType::Keyboard))) + format!("{:?}", syscall::device_claim(HANDLE_INVALID, DeviceRequest::Class(DeviceType::Framebuffer))) } "rt-absent" => format!("{:?}", syscall::rt_enter(HANDLE_INVALID)), // `HANDLE_INVALID` with room for one entry. The same number is the diff --git a/tests/guest/rust-tests/src/bin/stick_load.rs b/tests/guest/rust-tests/src/bin/stick_load.rs new file mode 100644 index 00000000000..0d413dcb3e0 --- /dev/null +++ b/tests/guest/rust-tests/src/bin/stick_load.rs @@ -0,0 +1,17 @@ +//! Writes the boot stick through usbd for ever, so the boot deadline resets +//! the machine with a write in flight: `stick_survives_a_reset_under_load`'s +//! load. Lines to `/log` ([`stick_lines`]), which logkeeper writes to the +//! stick and syncs every round. + +#[path = "../stick_lines.rs"] +mod stick_lines; + +/// About 180 KB a second: a write and a sync on the stick every logkeeper +/// round, and a boot's log that stays inside logkeeper's retention (16 files +/// of a mebibyte) until the deadline ends it, so the file read back is +/// whole. At ten times the rate the boot rotated 55 of its own parts away. +const PER_SECOND: u64 = 200; + +fn main() { + stick_lines::say("stick_load", u64::MAX, PER_SECOND, || true); +} diff --git a/tests/guest/rust-tests/src/bin/stick_write.rs b/tests/guest/rust-tests/src/bin/stick_write.rs new file mode 100644 index 00000000000..31e0bd0e103 --- /dev/null +++ b/tests/guest/rust-tests/src/bin/stick_write.rs @@ -0,0 +1,14 @@ +//! Eight mebibytes of lines to `/log` on the boot stick through usbd +//! ([`stick_lines`]), then an end: the stick-write load `mask_windows` reads +//! the windows under. + +#[path = "../stick_lines.rs"] +mod stick_lines; + +/// About eight mebibytes. +const LINES: u64 = (8 << 20) / stick_lines::LINE_BYTES as u64; + +fn main() { + let said = stick_lines::say("stick_write", LINES, 2000, || true); + println!("stick_write: {said} lines of {} bytes said", stick_lines::LINE_BYTES); +} diff --git a/tests/guest/rust-tests/src/bin/tone_beside_stick_write.rs b/tests/guest/rust-tests/src/bin/tone_beside_stick_write.rs new file mode 100644 index 00000000000..f9cb7cc4dc8 --- /dev/null +++ b/tests/guest/rust-tests/src/bin/tone_beside_stick_write.rs @@ -0,0 +1,32 @@ +//! `hda_tone`'s tone, played while a thread writes lines to `/log` on the +//! boot stick through usbd ([`stick_lines`]): `tone_beside_a_stick_write`'s +//! stimulus (`issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md`). +//! The judge counts the lines read back off the stick, which have to be the +//! mebibyte the issue names; soundserver's counters over the job's window are +//! the verdict. + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +use cpal::traits::{DeviceTrait, HostTrait}; + +#[path = "../tone.rs"] +mod tone; + +#[path = "../stick_lines.rs"] +mod stick_lines; + +fn main() { + let device = cpal::default_host().default_output_device().expect("no audio output device"); + let config = device.default_output_config().expect("no audio config"); + let playing = Arc::new(AtomicBool::new(true)); + let writer = { + let playing = playing.clone(); + std::thread::spawn(move || stick_lines::say("tone_beside_stick_write", u64::MAX, 2000, || playing.load(Ordering::Relaxed))) + }; + tone::play(&device, config.into()); + playing.store(false, Ordering::Relaxed); + let said = writer.join().expect("tone_beside_stick_write: the writer panicked"); + println!("tone done"); + println!("tone_beside_stick_write: {said} lines of {} bytes said beside the tone", stick_lines::LINE_BYTES); +} diff --git a/tests/guest/rust-tests/src/bin/usbd_spare_unhanded.rs b/tests/guest/rust-tests/src/bin/usbd_spare_unhanded.rs deleted file mode 100644 index af4c40d0870..00000000000 --- a/tests/guest/rust-tests/src/bin/usbd_spare_unhanded.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! `usbd_spare`'s `unhanded` arm as a binary of its own, for a metal job, which -//! takes no argument: see `src/usbd_spare.rs`. - -#[path = "../usbd_spare.rs"] -#[allow(dead_code, reason = "this binary runs one of the module's arms")] -mod usbd_spare; - -fn main() { - usbd_spare::unhanded(&usbd_spare::cap()); -} diff --git a/tests/guest/rust-tests/src/spare.rs b/tests/guest/rust-tests/src/spare.rs index 767a6b8ac19..a6a0a58bee9 100644 --- a/tests/guest/rust-tests/src/spare.rs +++ b/tests/guest/rust-tests/src/spare.rs @@ -1,6 +1,5 @@ //! The spare controller a job claims: the one function of any of a set of -//! cards the kernel does not drive, or, on a kernel that drives every one, -//! the only one there is. Named by its arrival, because a machine may have +//! cards no driver holds. Named by its arrival, because a machine may have //! two of one card. use toyos::syscap::SysCap; @@ -9,6 +8,11 @@ use toyos_abi::syscall::PciId; /// That function, or `None` where it is not one. pub fn of(cap: &SysCap, ids: &[PciId]) -> Option { + one(cap, ids, Driven::Free) +} + +/// The one function of `ids` that `driven` says of. +pub fn one(cap: &SysCap, ids: &[PciId], driven: Driven) -> Option { let records: Vec = cap.records(|n| vec![RawRecord::EMPTY; n]).unwrap_or_else(|why| panic!("the job's capability reads no inventory: {why:?}")); let spares: Vec = records @@ -18,9 +22,8 @@ pub fn of(cap: &SysCap, ids: &[PciId]) -> Option { _ => None, }) .collect(); - let left: Vec = spares.iter().copied().filter(|pci| pci.driven != Driven::Kernel).collect(); - match (left.as_slice(), spares.as_slice()) { - ([one], _) | ([], [one]) => Some(*one), + match spares.iter().copied().filter(|pci| pci.driven == driven).collect::>()[..] { + [one] => Some(one), _ => None, } } diff --git a/tests/guest/rust-tests/src/stick_lines.rs b/tests/guest/rust-tests/src/stick_lines.rs new file mode 100644 index 00000000000..ac26db43d2f --- /dev/null +++ b/tests/guest/rust-tests/src/stick_lines.rs @@ -0,0 +1,33 @@ +//! Lines written to `/log`, which a metal boot's logkeeper keeps on the boot +//! stick and makes durable every round: the one volume on that stick a job +//! reaches. (`/home` is DATA's, and a metal test image carries no DATA +//! partition, so DATA lives in memory there.) +//! +//! Each line is one record of [`LINE_BYTES`], paced at the caller's rate, +//! under logkeeper's allowance for a program's children (4096 records a +//! second), so none is suppressed and every one reaches the file. What +//! reached the medium is counted by the judge in the file read back off the +//! stick, never here. + +use std::time::{Duration, Instant}; + +/// The bytes one line carries, its prefix and number included. +pub const LINE_BYTES: usize = 900; +/// Say lines under `name`, `per_second` of them, until `go_on` answers false or +/// `most` are said; answers how many were. +pub fn say(name: &str, most: u64, per_second: u64, go_on: impl Fn() -> bool) -> u64 { + let began = Instant::now(); + let mut said = 0u64; + while said < most && go_on() { + let head = format!("{name}: line {said} "); + println!("{head}{}", "x".repeat(LINE_BYTES.saturating_sub(head.len()))); + said += 1; + // Paced to the rate, not slept by a fixed amount: a line late does + // not make the next one later. + let due = began + Duration::from_micros(said * 1_000_000 / per_second); + if let Some(wait) = due.checked_duration_since(Instant::now()) { + std::thread::sleep(wait); + } + } + said +} diff --git a/tests/guest/rust-tests/src/usbd_spare.rs b/tests/guest/rust-tests/src/usbd_spare.rs index 574d711c8b5..68f7978c5be 100644 --- a/tests/guest/rust-tests/src/usbd_spare.rs +++ b/tests/guest/rust-tests/src/usbd_spare.rs @@ -1,8 +1,8 @@ //! `usbd_spare`'s arms, one binary each for a metal job and all of them -//! behind one for QEMU. The xHCI controller the kernel was told to leave -//! alone (`xhci-leave=`), claimed by this job and driven by `/system/bin/usbd`, which it starts -//! itself so it can end it: this job holds the claim, the port usbd answers -//! `inspect` on, and usbd's process. +//! behind one for QEMU. The xHCI controller no `drives` row of the image +//! names, claimed by this job and driven by `/system/bin/usbd`, which it +//! starts itself so it can end it: this job holds the claim, the port usbd +//! answers `inspect` on, and usbd's process. //! //! - no argument: usbd started, asked once it says it has settled whether its //! No-Op was answered by an interrupt through the claim, asked again across @@ -11,7 +11,8 @@ //! - `fault`: the claim's function aimed, with no usbd, at an address outside //! every grant: the kernel's `DMA FAULT` record is the harness's to read, //! and the claim refusing its interrupt read from then on is this job's. -//! - `refused`: the claim, on a kernel that drives the function, refused. +//! - `refused`: a claim on the boot controller, which the image's usbd +//! holds, refused. //! - `unhanded`: the claim, on a machine that will not hand it over, //! refused; the kernel's line says why. //! - `vector`: the claim's MSI-X table entry rewritten, by this job through @@ -112,15 +113,15 @@ impl Usbd { Self { child, said, conn } } - /// Wait for usbd's line starting `prefix`, bounded. - fn until(&self, prefix: &str) -> String { + /// Wait for usbd's line saying `what`, bounded. + fn until(&self, what: &str) -> String { let deadline = Instant::now() + SETTLE_BOUND; loop { let left = deadline.saturating_duration_since(Instant::now()); match self.said.recv_timeout(left) { - Ok(line) if line.starts_with(prefix) => return line, + Ok(line) if line.starts_with("usbd ") && line.contains(what) => return line, Ok(_) => {} - Err(why) => panic!("usbd_spare: usbd never said {prefix:?} within {SETTLE_BOUND:?}: {why:?}"), + Err(why) => panic!("usbd_spare: usbd never said {what:?} within {SETTLE_BOUND:?}: {why:?}"), } } } @@ -172,7 +173,7 @@ pub fn drive(cap: &SysCap) { let (id, claimed) = claim(cap); let dev = claimed.unwrap_or_else(|why| panic!("usbd_spare: the claim on {} was refused: {why:?}", spelled(id))); let usbd = Usbd::start(id, dev); - usbd.until("usbd: settled"); + usbd.until(": settled"); let first = usbd.ask("settled"); assert_eq!(number(&first, "usb.events.unannounced"), 0, "usbd_spare: an event no interrupt announced"); assert_eq!(number(&first, "usb.operations.silent"), 0, "usbd_spare: an operation nothing answered"); @@ -234,12 +235,17 @@ pub fn fault(cap: &SysCap) { } } +/// QEMU's boot controller, which the image's usbd row drives. +const BOOT: [PciId; 1] = [PciId { vendor: 0x1033, device: 0x0194 }]; + pub fn refused(cap: &SysCap) { - let (id, claimed) = claim(cap); - match claimed { - Err(SyscallError::PermissionDenied) => println!("usbd_spare: the claim on {} was refused: PermissionDenied", spelled(id)), - Err(why) => panic!("usbd_spare: the claim was refused, by {why:?} and not because the kernel drives it"), - Ok(_) => panic!("usbd_spare: the claim on {} was granted, on a kernel that drives it", spelled(id)), + let boot = spare::one(cap, &BOOT, toyos_abi::inventory::Driven::Claimed) + .expect("usbd_spare: no boot controller a process holds"); + let id = PciId { vendor: boot.vendor, device: boot.device }; + match cap.claim_function::(boot.arrival) { + Err(SyscallError::AlreadyExists) => println!("usbd_spare: the claim on {} was refused: AlreadyExists", spelled(id)), + Err(why) => panic!("usbd_spare: the claim was refused, by {why:?} and not because usbd holds it"), + Ok(_) => panic!("usbd_spare: the claim on {} was granted, while usbd holds it", spelled(id)), } } @@ -281,7 +287,7 @@ pub fn vector(cap: &SysCap) { let mut usbd = Usbd::start(id, dev); let said = usbd.until_ended(); assert!( - !said.iter().any(|line| line.starts_with("usbd: a No-Op command completed")), + !said.iter().any(|line| line.starts_with("usbd ") && line.contains(": a No-Op command completed")), "usbd_spare: usbd was told of its No-Op through a vector not its own" ); let status = usbd.child.wait().expect("usbd_spare: wait for usbd"); diff --git a/tests/images/acpicase.toml b/tests/images/acpicase.toml index d40321ce61a..86bc1a7e675 100644 --- a/tests/images/acpicase.toml +++ b/tests/images/acpicase.toml @@ -44,6 +44,14 @@ serves = ["block", "disk"] drives = ["pci:01:08:02/1b36:0010"] receives = ["disk"] +# The USB driver, one process per boot controller: QEMU's `nec-usb-xhci`, the +# T14's 00:14.0 and the AMD bench's 03:00.3. QEMU's spare, the T14's Type-C +# controller and the AMD bench's 03:00.4 stay free for the tests that claim +# them. +[programs.usbd] +drives = ["pci:0c:03:30/1033:0194", "pci:0c:03:30/8086:a0ed", "pci:0c:03:30/1022:1639@03:00.3"] +receives = ["disk"] + # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when # one ends, on the same ports. diff --git a/tests/images/clipboardcase.toml b/tests/images/clipboardcase.toml index 7a29eab3b85..dcb0294d230 100644 --- a/tests/images/clipboardcase.toml +++ b/tests/images/clipboardcase.toml @@ -23,7 +23,7 @@ syscap = ["logread"] [programs.compositor] service = true serves = ["compositor", "prompt", "clipboard", "input", "display"] -devices = ["pci:03:80:00/1af4:1050", "keyboard", "mouse"] +devices = ["pci:03:80:00/1af4:1050"] args = ["scale=2"] # One process per virtio-serial function the kernel does not drive: the diff --git a/tests/images/jobcase.toml b/tests/images/jobcase.toml index a69765133fe..b88eeba7cf2 100644 --- a/tests/images/jobcase.toml +++ b/tests/images/jobcase.toml @@ -35,6 +35,14 @@ serves = ["block", "disk"] drives = ["pci:01:08:02/1b36:0010"] receives = ["disk"] +# The USB driver, one process per boot controller: QEMU's `nec-usb-xhci`, the +# T14's 00:14.0 and the AMD bench's 03:00.3. QEMU's spare, the T14's Type-C +# controller and the AMD bench's 03:00.4 stay free for the tests that claim +# them. +[programs.usbd] +drives = ["pci:0c:03:30/1033:0194", "pci:0c:03:30/8086:a0ed", "pci:0c:03:30/1022:1639@03:00.3"] +receives = ["disk"] + # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when # one ends, on the same ports. diff --git a/tests/images/latencycase.toml b/tests/images/latencycase.toml index ea1173b747f..b813f0c99dd 100644 --- a/tests/images/latencycase.toml +++ b/tests/images/latencycase.toml @@ -43,6 +43,14 @@ serves = ["block", "disk"] drives = ["pci:01:08:02/1b36:0010"] receives = ["disk"] +# The USB driver, one process per boot controller: QEMU's `nec-usb-xhci`, the +# T14's 00:14.0 and the AMD bench's 03:00.3. QEMU's spare, the T14's Type-C +# controller and the AMD bench's 03:00.4 stay free for the tests that claim +# them. +[programs.usbd] +drives = ["pci:0c:03:30/1033:0194", "pci:0c:03:30/8086:a0ed", "pci:0c:03:30/1022:1639@03:00.3"] +receives = ["disk"] + # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when # one ends, on the same ports. diff --git a/tests/images/metalcase.toml b/tests/images/metalcase.toml index 0d36127c7b0..f5efb61b158 100644 --- a/tests/images/metalcase.toml +++ b/tests/images/metalcase.toml @@ -28,7 +28,7 @@ serves = ["log"] service = true serves = ["compositor", "prompt", "clipboard", "input", "display"] receives = ["soundserver"] -devices = ["framebuffer", "keyboard", "mouse"] +devices = ["framebuffer"] [programs.soundserver] service = true @@ -88,6 +88,14 @@ serves = ["block", "disk"] drives = ["pci:01:08:02/1b36:0010"] receives = ["disk"] +# The USB driver, one process per boot controller: QEMU's `nec-usb-xhci`, the +# T14's 00:14.0 and the AMD bench's 03:00.3. QEMU's spare, the T14's Type-C +# controller and the AMD bench's 03:00.4 stay free for the tests that claim +# them. +[programs.usbd] +drives = ["pci:0c:03:30/1033:0194", "pci:0c:03:30/8086:a0ed", "pci:0c:03:30/1022:1639@03:00.3"] +receives = ["disk", "input"] + # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when # one ends, on the same ports. diff --git a/tests/images/metaldevicecase.toml b/tests/images/metaldevicecase.toml index 3e99260e4f1..94148afde3b 100644 --- a/tests/images/metaldevicecase.toml +++ b/tests/images/metaldevicecase.toml @@ -76,6 +76,14 @@ serves = ["block", "disk"] drives = ["pci:01:08:02/1b36:0010"] receives = ["disk"] +# The USB driver, one process per boot controller: QEMU's `nec-usb-xhci`, the +# T14's 00:14.0 and the AMD bench's 03:00.3. QEMU's spare, the T14's Type-C +# controller and the AMD bench's 03:00.4 stay free for the tests that claim +# them. +[programs.usbd] +drives = ["pci:0c:03:30/1033:0194", "pci:0c:03:30/8086:a0ed", "pci:0c:03:30/1022:1639@03:00.3"] +receives = ["disk"] + # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when # one ends, on the same ports. diff --git a/tests/images/netcase.toml b/tests/images/netcase.toml index 0f89cbad160..76488ae3139 100644 --- a/tests/images/netcase.toml +++ b/tests/images/netcase.toml @@ -50,6 +50,14 @@ serves = ["block", "disk"] drives = ["pci:01:08:02/1b36:0010"] receives = ["disk"] +# The USB driver, one process per boot controller: QEMU's `nec-usb-xhci`, the +# T14's 00:14.0 and the AMD bench's 03:00.3. QEMU's spare, the T14's Type-C +# controller and the AMD bench's 03:00.4 stay free for the tests that claim +# them. +[programs.usbd] +drives = ["pci:0c:03:30/1033:0194", "pci:0c:03:30/8086:a0ed", "pci:0c:03:30/1022:1639@03:00.3"] +receives = ["disk"] + # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when # one ends, on the same ports. diff --git a/tests/images/panelcase.toml b/tests/images/panelcase.toml index b8a7b2ab5df..169e69f0565 100644 --- a/tests/images/panelcase.toml +++ b/tests/images/panelcase.toml @@ -17,4 +17,4 @@ syscap = ["logread"] [programs.compositor] service = true serves = ["compositor", "prompt", "clipboard", "input", "display"] -devices = ["framebuffer", "keyboard", "mouse"] +devices = ["framebuffer"] diff --git a/tests/images/proctreecase.toml b/tests/images/proctreecase.toml index 9f51cb8a43b..958ab398877 100644 --- a/tests/images/proctreecase.toml +++ b/tests/images/proctreecase.toml @@ -63,6 +63,14 @@ serves = ["block", "disk"] drives = ["pci:01:08:02/1b36:0010"] receives = ["disk"] +# The USB driver, one process per boot controller: QEMU's `nec-usb-xhci`, the +# T14's 00:14.0 and the AMD bench's 03:00.3. QEMU's spare, the T14's Type-C +# controller and the AMD bench's 03:00.4 stay free for the tests that claim +# them. +[programs.usbd] +drives = ["pci:0c:03:30/1033:0194", "pci:0c:03:30/8086:a0ed", "pci:0c:03:30/1022:1639@03:00.3"] +receives = ["disk"] + # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when # one ends, on the same ports. diff --git a/tests/images/scalecase.toml b/tests/images/scalecase.toml index b48e8416fed..8bb5db7740d 100644 --- a/tests/images/scalecase.toml +++ b/tests/images/scalecase.toml @@ -19,7 +19,7 @@ syscap = ["logread"] [programs.compositor] service = true serves = ["compositor", "prompt", "clipboard", "input", "display"] -devices = ["framebuffer", "keyboard", "mouse"] +devices = ["framebuffer"] args = ["scale=2"] [programs.ps2server] diff --git a/tests/images/testcases.toml b/tests/images/testcases.toml index e773190987e..51070fdfc4d 100644 --- a/tests/images/testcases.toml +++ b/tests/images/testcases.toml @@ -70,12 +70,6 @@ syscap = ["device", "dup", "logread", "power", "roster", "counters", "trace", "i [programs.toybox] receives = ["soundserver"] -# One xHCI controller driven from userland: `usbd_spare` claims the controller -# the kernel was told to leave alone (`xhci-leave=`, `pci:1b36:000d` on the -# guest and the Type-C controller on the T14), and starts and ends this itself, -# holding the claim, its `usb` port and its process. -[programs.usbd] - [symlinks] "bin/cat" = "/system/bin/toybox" "bin/cp" = "/system/bin/toybox" @@ -110,6 +104,15 @@ serves = ["block", "disk"] drives = ["pci:01:08:02/1b36:0010"] receives = ["disk"] +# The USB driver, one process per boot controller: QEMU's `nec-usb-xhci`, the +# T14's 00:14.0 and the AMD bench's 03:00.3. QEMU's spare, the T14's Type-C +# controller and the AMD bench's 03:00.4 stay free: `usbd_spare` claims the +# one a machine has and starts its own usbd on it, holding the claim, its +# `usb` port and its process. +[programs.usbd] +drives = ["pci:0c:03:30/1033:0194", "pci:0c:03:30/8086:a0ed", "pci:0c:03:30/1022:1639@03:00.3"] +receives = ["disk"] + # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when # one ends, on the same ports. diff --git a/tests/metal/src/devices.rs b/tests/metal/src/devices.rs deleted file mode 100644 index e5f2bd75c65..00000000000 --- a/tests/metal/src/devices.rs +++ /dev/null @@ -1,192 +0,0 @@ -//! What a metal boot says about itself: the account the shutdown seals into -//! the black box. -//! -//! **Text in, verdicts out.** Everything here reads the two strings -//! the loop (`lib.rs`) brings back off the stick — the loader's file and `logkeeper`'s — -//! and nothing here touches a machine. - -#![forbid(unsafe_code)] - -/// The head of the reset's own account, as `kernel/src/drivers/xhci/stop.rs` -/// spells it. -pub const QUIESCE_HEAD: &str = "usb-quiesce:"; - -/// What the stop calls the thing it settles before it touches a port. Every -/// reset says this once, whether a device was inside one or not. -pub const QUIESCE_COMMAND: &str = "Bulk-Only command"; - -/// What the stop says about the endpoint the controller had when it ran — the -/// one line in the account that is the hardware's word and not the driver's. -pub const QUIESCE_ENDPOINT: &str = "the controller had that device's data endpoint"; - -/// What the shutdown did, out of its summary line. -/// -/// **Read as pairs and never as totals**, because "one controller halted" and -/// "one of two controllers halted" are the difference the whole path exists -/// for. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Quiesced { - pub flushed: u32, - pub disks: u32, - /// Of those disks, how many answered INVALID COMMAND OPERATION CODE to - /// SYNCHRONIZE CACHE. **Reported and never judged**: a device with no write - /// cache made nothing durable by refusing, so the flush is `ok` and means a - /// different thing — and on the T14 this is every disk, which is why the - /// count is here rather than left to the word `ok`. - pub cacheless: u32, - /// Connected ports whose reset finished, of the connected ports there were. - /// **The act an attached device sees**, and the one that ends a transfer. - pub reset_ports: u32, - pub connected: u32, - pub halted: u32, - pub controllers: u32, - pub reset: u32, - pub unpowered: u32, - pub ports: u32, -} - -impl Quiesced { - /// Whether every device the boot had was handed back: every disk's cache - /// emptied, every connected port reset, every controller halted and reset. - /// - /// Port *power* is reported and not judged: `PORTSC.PP` is writable only on - /// a controller with Port Power Control, so a count under the total is a - /// fact about the silicon and not about the shutdown. The port *reset* is - /// judged, because it is the one act an attached device sees and no - /// controller may decline it. - pub fn complete(&self) -> bool { - self.flushed == self.disks - && self.reset_ports == self.connected - && self.halted == self.controllers - && self.reset == self.controllers - && self.controllers > 0 - } -} - -/// The shutdown's summary out of `loader.log`, or `None` where the pass carries -/// none. -pub fn quiesced(loader: &str) -> Option { - let line = loader.lines().find(|l| l.contains(" disk cache(s) flushed"))?; - let pair = |what: &str| -> Option<(u32, u32)> { - let (a, b) = line.split(what).next()?.split_whitespace().next_back()?.split_once('/')?; - Some((a.parse().ok()?, b.parse().ok()?)) - }; - let (flushed, disks) = pair(" disk cache(s) flushed")?; - let cacheless: u32 = - line.split(" with no cache to flush").next()?.split_whitespace().next_back()?.parse().ok()?; - let (reset_ports, connected) = pair(" connected port(s) reset")?; - let (halted, controllers) = pair(" controller(s) halted")?; - let (unpowered, ports) = pair(" port(s) unpowered")?; - let reset = line.split(" controller(s) halted, ").nth(1)?.split_whitespace().next()?; - Some(Quiesced { - flushed, - disks, - cacheless, - reset_ports, - connected, - halted, - controllers, - reset: reset.parse().ok()?, - unpowered, - ports, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// `loader.log`'s pass after the reset, with the stop's tail and the - /// shutdown's own account under the `|` the loader prefixes a report's - /// lines with. - fn a_good_loader() -> String { - "ToyOS Bootloader 1.0\n\ - Black box: the last boot read DONE, so it handed the machine back on purpose and this \ - chain ends here\n\ - | log: this boot's newest records follow, newest first\n\ - | log-tail: [ 3.960 cpu0 kernel] Rebooting.\n\ - | log-tail: [ 3.955 cpu0 kernel] usb-quiesce: disk 0 SYNCHRONIZE CACHE ok\n\ - | usb-quiesce: no Bulk-Only command was open, so this reset cuts none\n\ - | usb-quiesce: xHCI 00:14.0 halted=true USBSTS=0x00000009\n\ - | usb-quiesce: 2/2 disk cache(s) flushed, 1 with no cache to flush, \ - 5/5 connected port(s) reset, 2/2 controller(s) halted, 2 reset, \ - 12/16 port(s) unpowered\n\ - Loader log: the last boot is accounted for, so this pass resets the machine\n" - .to_string() - } - - #[test] - fn the_shutdowns_own_account_is_read_out_of_the_loaders_file() { - assert_eq!( - quiesced(&a_good_loader()), - Some(Quiesced { - flushed: 2, - disks: 2, - cacheless: 1, - reset_ports: 5, - connected: 5, - halted: 2, - controllers: 2, - reset: 2, - unpowered: 12, - ports: 16, - }) - ); - assert!(quiesced(&a_good_loader()).unwrap().complete()); - // Ports are reported, not judged: a controller with no Port Power - // Control ignores the write and 0/16 is the silicon's answer. - assert!(quiesced(&a_good_loader().replace("12/16 port", "0/16 port")).unwrap().complete()); - // Each of the four that *are* judged, failing on its own. - for (from, to) in [ - ("2/2 disk", "1/2 disk"), - ("5/5 connected", "4/5 connected"), - ("2/2 controller(s) halted", "1/2 controller(s) halted"), - ("halted, 2 reset", "halted, 1 reset"), - // A shutdown that found no controller is not one that handed - // everything back; it is a machine this table is not about. - ("2/2 controller(s) halted, 2 reset", "0/0 controller(s) halted, 0 reset"), - ] { - let moved = a_good_loader().replace(from, to); - assert!(!quiesced(&moved).unwrap().complete(), "{from} -> {to}"); - } - assert_eq!(quiesced("ToyOS Bootloader 1.0\n"), None); - } - - /// **The lines this crate judges a reset by are the kernel's own**, and - /// nothing links the two: a reword at the site that did not come here would - /// leave every metal readback passing on a predicate that matches nothing. - #[test] - fn the_kernel_writes_the_quiesce_lines_the_host_reads() { - let at = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../kernel/src/drivers/xhci/stop.rs"); - let source = std::fs::read_to_string(&at).expect("the reset path"); - let written = code_of(&source); - for needle in [QUIESCE_HEAD, QUIESCE_COMMAND, QUIESCE_ENDPOINT] { - assert!(written.contains(needle), "{} does not write {needle:?}", at.display()); - } - } - - /// A line the kernel only *talks* about is not a line the kernel writes: - /// every needle above appears in that file's own prose as well, so a scan - /// over the raw source passes on a kernel that emits none of them. - #[test] - fn a_needle_that_only_appears_in_a_comment_is_not_found() { - assert_eq!(code_of(" //! a Bulk-Only command was open\n"), ""); - assert_eq!(code_of(" /// no Bulk-Only command was open\n"), ""); - assert_eq!(code_of(" // no Bulk-Only command was open\n"), ""); - assert!(code_of(" writeln!(said, \"no Bulk-Only command was open\");\n") - .contains(QUIESCE_COMMAND)); - } - - /// `source` with its whole-line comments taken out, which is every form - /// this tree's prose takes: a doc comment, a module header, a note above a - /// statement. A trailing comment after code is left, and cannot carry one - /// of these needles without the code above it on the same line. - fn code_of(source: &str) -> String { - source - .lines() - .filter(|line| !line.trim_start().starts_with("//")) - .collect::>() - .join("\n") - } -} diff --git a/tests/metal/src/lib.rs b/tests/metal/src/lib.rs index 6c02c8629ae..220fdbda60a 100644 --- a/tests/metal/src/lib.rs +++ b/tests/metal/src/lib.rs @@ -35,7 +35,6 @@ use toyos_build::image::LBA; use crate::timings::{Machine, Record, Stick}; -pub mod devices; pub mod image; pub mod testargs; pub mod timings; @@ -749,8 +748,6 @@ pub const FLASHABLE: &[&str] = &[ "revoked-backing-selftest", "lapic-spurious-selftest", "unclaimed-vector-selftest", - "xhci-xecp-selftest", - "xhci-descriptor-selftest", // A probe rather than staged inputs, and a read: it runs inside the first // syscall's own context switch. "sysret-ss-probe", @@ -761,16 +758,11 @@ pub const FLASHABLE: &[&str] = &[ // 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. - "sched-operation-nesting", // Every IOMMU unit left translating, remapping and queueing just before the // kernel programs it, which switches each of those off again first. It // writes only the units' registers, which a reset returns to firmware, and // tables in memory this boot owns. "iommu-firmware-left", - // No interrupt remapping: the DMAR's flags read with INTR_REMAP clear, or - // every AMD-Vi unit's interrupt halves left invalid. It writes no - // register a reset does not return to firmware. - "iommu-no-remap", // Every CPU is handed the loader's Intel update with its header's revision // raised by one and its checksum sealed again. Its payload is Intel's own, // the revision the CPU already runs, so the CPU refuses it or runs what it @@ -802,35 +794,6 @@ pub const FLASHABLE: &[&str] = &[ // register and writes no firmware state; the kernel implies `WEDGE_ARM` // behind it, so the boot cannot end itself before its own bound. LOCKUP_ARM, - // **The arm that stops nothing and never stops writing**, so the reset - // lands on a controller that is moving bytes. Admissible for the rows - // above's reason and one more: every run is read first and written back - // byte for byte in the last eighth of the disk, once and never twice, so - // the medium is what it was and no partition a boot mounts is the subject; - // and the sweep is refused by name on a disk with no room for it. - LOAD_ARM, - // It withholds transfers to the boot stick so the transport breaks on - // purpose. Admissible because it writes nothing the stick did not already - // 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 T14's Type-C xHCI controller, `00:0d.0`, 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 T14's two (each reads `USBLEGSUP 0x01002201`, every SMI - // enable clear). Whatever state a boot leaves it in, the reset that ends - // every boot resets the function and runs POST again, which takes it - // back. Only this address: the other controller carries the boot stick. - "xhci-leave=00:0d.0", - // The AMD bench's `03:00.4`, the xHCI the Bluetooth module is on, left to - // a claim the same way. Firmware owns it (`USBLEGSUP 0x00010401`) until - // `usbd` takes it; a boot may leave it halted with bus mastering off - // after the fault arm, or, where the claim is refused, still firmware's - // with its SMI on an ownership change armed. Either way the reset that - // ends every boot resets the function and runs POST again, which takes - // the controller back. Only this address: `03:00.3`, which shares its id, - // carries the boot stick. - "xhci-leave=03:00.4", ]; /// The arm that stops the machine, named once: [`FLASHABLE`] rules on it and @@ -843,17 +806,13 @@ pub const WEDGE_ARM: &str = "wedge-before-reset"; /// and which bound sealed the page is the page's own first line to say. pub const LOCKUP_ARM: &str = "hard-lockup-probe"; -/// The arm that leaves the machine to its bound with the bus busy rather than -/// idle: it stops no CPU and never stops writing. -pub const LOAD_ARM: &str = "usb-reset-under-load"; - /// Every arm that deliberately leaves this machine to be ended by a bound. /// /// **One list and not a condition spelled out at each reader.** A further such /// arm added to [`FLASHABLE`] and not here is an image whose boot is judged by /// the word a shutdown writes — which it never reaches — so the reader that /// forgot it reds every boot it was staged for. -pub const WEDGE_ARMS: &[&str] = &[WEDGE_ARM, LOCKUP_ARM, LOAD_ARM]; +pub const WEDGE_ARMS: &[&str] = &[WEDGE_ARM, LOCKUP_ARM]; /// The arm whose boot the pass after it reads as a hang on purpose: its record /// is sealed under another stick's identity, so that pass clears it and finds @@ -866,6 +825,21 @@ pub fn stages_a_wedge(armed: &[impl AsRef]) -> bool { armed.iter().any(|name| WEDGE_ARMS.contains(&name.as_ref())) } +/// Whether this image is ended by its own boot deadline on purpose: armed to +/// wedge, or carrying a deadline below the least bound a runner gives its list +/// ([`toyos_tco::JOB_BOUND_MS`]). The harness gives such a deadline only to a +/// boot whose load runs until the reset, such as `stick_survives_a_reset_under_load`'s, +/// so the boot is judged by the record that bound sealed, as a wedge is. +pub fn ended_by_its_deadline(armed: &[impl AsRef]) -> bool { + stages_a_wedge(armed) + || armed.iter().any(|name| { + name.as_ref() + .strip_prefix(toyos_tco::DEADLINE_PARAM) + .and_then(|ms| ms.parse::().ok()) + .is_some_and(|ms| ms < toyos_tco::JOB_BOUND_MS) + }) +} + /// The `boot-deadline=` bound an image armed with `armed` carries, whose /// runner bounds its list at `list_ms`. pub fn bound_for(armed: &[impl AsRef], list_ms: u64) -> u64 { @@ -1705,9 +1679,7 @@ pub fn run(args: &Args) -> Result, Refusal> { })?; } let complaints = toyos_fat32_check::check(&bytes); - if !complaints.is_empty() { - return Err(Refusal::Fat32(toyos_fat32_check::describe(&complaints))); - } + fat32_verdict(&armed, &complaints)?; println!("toyos-fat32-check: the log partition's {} bytes check out", bytes.len()); } let raw_log = args.fat32_check.then(|| began.elapsed().as_secs_f64()); @@ -1726,6 +1698,28 @@ pub fn run(args: &Args) -> Result, Refusal> { judge_and_write_readback(&armed, &loader, &log, args.readback.as_deref(), &boot).map(Some) } +/// The outside judge's verdict on the log partition. A boot that hands the +/// machine back owes a volume with nothing to say about it. One its own +/// deadline ends ([`ended_by_its_deadline`]) stopped wherever logkeeper's +/// append was, and owes only what such a stop can leave +/// ([`toyos_fat32_check::unexplained_by_a_stop`]), each of which is printed. +fn fat32_verdict(armed: &[String], complaints: &[toyos_fat32_check::Complaint]) -> Result<(), Refusal> { + let refused: Vec = if ended_by_its_deadline(armed) { + let unexplained = toyos_fat32_check::unexplained_by_a_stop(complaints); + for left in complaints.iter().filter(|c| !unexplained.iter().any(|u| std::ptr::eq(*u, *c))) { + println!("toyos-fat32-check: what the reset's stop left: {}", toyos_fat32_check::describe(std::slice::from_ref(left))); + } + unexplained.into_iter().cloned().collect() + } else { + complaints.to_vec() + }; + if refused.is_empty() { + Ok(()) + } else { + Err(Refusal::Fat32(toyos_fat32_check::describe(&refused))) + } +} + /// What [`Driver::flash`] took, on a run that was no rehearsal. struct Flashed { wipe: f64, @@ -1780,7 +1774,7 @@ fn boot_verdict(armed: &[String], loader: &str, log: &str) -> Result Vec { names.iter().map(|n| (*n).to_string()).collect() }; + assert!(ended_by_its_deadline(&armed(&["boot-deadline=40000"]))); + assert!(!ended_by_its_deadline(&armed(&["boot-deadline=120000"]))); + assert_eq!(boot_verdict(&armed(&["boot-deadline=40000"]), &loader, log), Ok(12400)); + assert!(matches!( + boot_verdict(&armed(&["boot-deadline=120000"]), &loader, log), + Err(Refusal::Log(bootlog::Unfit::Unfinished(_))) + )); + let handed_back = format!( + "{}Black box: {} at 2026-10-21-090207\n| {}[52.400 cpu0 kernel] {}\n", + loader, + bootlog::HANDED_BACK, + bootlog::LOG_TAIL, + bootlog::REBOOTING + ); + assert!(matches!( + boot_verdict(&armed(&["boot-deadline=40000"]), &handed_back, log), + Err(Refusal::Wedge { .. }) + )); + } + /// The foreign-record arm's pass after the reset: the hang it stages is its /// verdict's premise, the record it cleared says the stop finished, and the /// same pass under any other arm is the hang refusal. diff --git a/tests/toyos.rs b/tests/toyos.rs index 33e090c1051..3e3ec8bceae 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -14,7 +14,7 @@ use common::qemu::{ self, await_guest, await_marker, await_marker_new, BootOptions, QemuInstance, STALLED, TIMED_OUT, }; -use common::{audio, claims, compile, devices, faults, isa, metal, power, ps2server, screen, serial, usb}; +use common::{audio, claims, compile, devices, faults, isa, metal, power, ps2server, screen, serial}; use toyos_build::bootlog::{self}; use toyos_metal::testargs::{self, SUITE}; @@ -67,6 +67,12 @@ const ACTUATOR_KERNEL: &[&str] = toyos_build::build::TEST_KERNEL; // Rust helper binaries that are spawned by tests, not tests themselves. const RUST_SKIP: &[&str] = &[ + // It writes the stick for ever, so that its boot's deadline resets the + // machine under it: `stick_survives_a_reset_under_load` runs it alone. + "stick_load", + // The stick-write load `mask_windows` reads windows under, on the boot + // whose kernel reports them. + "stick_write", // It opens a window, which needs a compositor the shared boot does not // run: `a_window_is_drawn_at_the_scale_and_relaid_at_the_next` runs it on // tests/images/scalecase.toml, and the `gop_edid_scale` metal row on @@ -123,8 +129,7 @@ const RUST_SKIP: &[&str] = &[ // `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. + // `claim_reuses_its_remapping_entry` metal row runs it. "pci_reclaim", // It measures the T14's I219, which no guest has: the // `i219_messages_on_its_msi_vector` metal row runs it. @@ -169,6 +174,7 @@ const RUST_SKIP: &[&str] = &[ // on QEMU's. "audio_tone", "audio_tone_resampled", + "tone_beside_stick_write", "volume_step", // It hands the ACPI claim to a server of its own and kills it, which needs // a boot that starts none: the `acpi_server_death` metal row runs it on @@ -211,17 +217,15 @@ const RUST_SKIP: &[&str] = &[ // It needs a launcher: `a_launch_gives_each_unsent_stdio_slot_a_null` // runs it on tests/images/launchcase.toml. "launch_unsent_stdio", - // It claims the xHCI controller `xhci-leave=` leaves alone, which no other - // boot's kernel does: `usbd_drives_the_spare` and the + // It claims the xHCI controller no image's usbd row names, which only a + // machine with a spare has: `usbd_drives_the_spare` and the // `usbd_drives_the_type_c_controller` metal row run it. "usbd_spare", - // `usbd_spare`'s fault, foreign-vector and refusal arms, one binary each - // because a metal job takes no argument: the AMD bench's - // `amdvi_usbd_drives_its_own_entry` and - // `amdvi_claim_refused_without_remapping` rows run them. + // `usbd_spare`'s fault and foreign-vector arms, one binary each because a + // metal job takes no argument: the AMD bench's + // `amdvi_usbd_drives_its_own_entry` row runs them. "usbd_spare_fault", "usbd_spare_vector", - "usbd_spare_unhanded", // The same controller, handed to a holder that is killed and claimed // again at once, in the same boots as `usbd_spare`. "end_returns_claims", @@ -484,6 +488,15 @@ const MACHINE_TESTS: &[&str] = &[ // `images/console.toml`'s image, which runs no job and hands no machine // back: a metal boot of it ends with a hand on the power button. "console_image_boots", + // The boot's log written through usbd onto the stick it booted from, and + // still there once the machine has stopped: the path every metal boot's + // `/log` takes, which no other QEMU row reads, because their console is + // the serial line. Its oracle is the image read back on the host. + "the_log_lands_on_the_boot_stick", + // Keys QEMU's `usb-kbd` reports, through usbd, to the owner of input: + // the console image's shell runs the command they spell. QEMU's xHC and + // keyboard are the independent end of the report the stub cannot be. + "a_usb_key_reaches_the_console", // The desktop on CPUs that offer no x2APIC, whose local APICs run in // xAPIC mode: every IPI, EOI and timer write goes through the MMIO page, // which no host build has and the T14, whose CPUs offer x2APIC, never @@ -505,12 +518,11 @@ const MACHINE_TESTS: &[&str] = &[ // A launch's unsent stdio slots: its own boot, because the launcher it // needs makes every spawn of a declared program a launch. "a_launch_gives_each_unsent_stdio_slot_a_null", - // A controller the kernel leaves alone, handed over to a claim whose - // holder's No-Op is answered by an interrupt through it, and a transfer - // aimed past the claim's grants: the hand-over, the interrupt through the - // claim and the unit have no host build, and the T14's unit is not asked - // to fault. What usbd names on the bus is `toyos-xhci`'s and its - // simulator's to hold. + // A controller no row names, handed over to a claim whose holder's No-Op + // is answered by an interrupt through it, and a transfer aimed past the + // claim's grants: the hand-over, the interrupt through the claim and the + // unit have no host build, and the T14's unit is not asked to fault. What + // usbd does on the bus is `toyos-xhci`'s stub tests' to hold. "usbd_drives_the_spare", // The same behind QEMU's AMD unit, remapping and with no SMI filter: the // claim is refused by that reason before anything on the function @@ -518,13 +530,9 @@ const MACHINE_TESTS: &[&str] = &[ // filter. "usbd_refused_without_smi_filter", // The same machine with its AMD unit remapping nothing: the claim is - // refused by that reason. The AMD bench's arm is - // `amdvi_claim_refused_without_remapping`. + // refused by that reason. No metal row asks it: a bench without remapping + // drives no USB, so no `/log` reaches its stick to judge. "usbd_refused_without_amdvi_remapping", - // A boot keyboard reporting more keys than its six slots name: the - // kernel's driver reading a report a device delivered over xHCI has no host - // build, and the T14 binds no USB keyboard. - "usb_keyboard_rollover", // Every vector register a thread holds, across switches to threads holding // others, against a scheduler, which no host build has. The shared boot's // run is the benches'; this is TCG's, whose XSAVE is QEMU's own reading of @@ -642,6 +650,22 @@ const METAL: &[(&str, metal::Metal)] = &[ }, }, ), + ( + // `hda_tone`'s tone while a thread of its process writes the stick + // through usbd: no period soundserver had to fill + // (`issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md`'s + // exit). It is the kernel's: one claim's interrupts and its holder's + // threads never starve another holder's deadline. The T14 and the AMD + // bench. + "tone_beside_a_stick_write", + metal::Metal { + arms: TESTCASES, + judge: |b| { + b[0].job_passed(audio::TONE_BESIDE_STICK_WRITE)?; + audio::tone_beside_a_stick_write(&b[0].log()) + }, + }, + ), ( // A tone at the device rate the device does not run, through cpal, // played to its end with every period covered: soundserver resamples it. @@ -931,23 +955,6 @@ const METAL: &[(&str, metal::Metal)] = &[ judge: |b| power::deadline_wedge_chain(&b[0].after_the_reset()?), }, ), - ( - // One boot: a machine writing to the stick continuously, reset out from - // under itself by the deadline with the controller mid-transfer, and - // the stick enumerable on the next host afterwards. - "usb_reset_records_the_phase_it_cut", - metal::Metal { - arms: &[metal::once("usbload", "tests/images/jobcase.toml", &["usb-reset-under-load"], &[])], - judge: |b| power::usb_load_chain(&b[0].after_the_reset()?), - }, - ), - ( - "usb_stick_left", - metal::Metal { - arms: &[metal::once("usbbreak", "tests/images/jobcase.toml", &["usb-transport-break"], &[])], - judge: |b| usb::transport_break_on_metal(&b[0].kernel(), &b[0].after_the_reset()?), - }, - ), ( // Its own boot, and the one arm in this profile the machine itself is // the instrument for: QEMU's TCG guest has no performance counter, so @@ -1052,16 +1059,6 @@ const METAL: &[(&str, metal::Metal)] = &[ judge: |b| microcode_on_metal(b[0].loader().text(), b[0].kernel().text(), b[0].cpus()?), }, ), - ( - // Two boots the suite already flashes: the device boot writes and - // fsyncs megabytes before its reset, and `metalcase` is the same reset - // with nothing written behind it but the hold's line. - "usb_reset_hands_devices_back", - metal::Metal { - arms: USB_RESET_BOOTS, - judge: power::usb_reset_on_metal, - }, - ), ( // On the T14 the chain is what every metal boot does — the loader // points `BootNext` at itself before each handoff, so the pass that @@ -1102,32 +1099,12 @@ const METAL: &[(&str, metal::Metal)] = &[ "lapic_spurious_vector", metal::Metal { arms: SELFTESTS, judge: |b| lapic_vectors(b[0].kernel().text()) }, ), - ( - // The T14's own controller publishes a real capability list, which is - // the half of this QEMU cannot give: q35's nec-usb-xhci has no USB - // Legacy Support capability in it at all. - "xhci_xecp_walk", - metal::Metal { arms: SELFTESTS, judge: |b| xhci_xecp(b[0].kernel().text()) }, - ), - ( - // Same: the crafted nine are the point, and beside them the parser - // binds a boot stick off a descriptor a real controller delivered. - "xhci_descriptor_walk", - metal::Metal { arms: SELFTESTS, judge: |b| xhci_descriptors(b[0].kernel().text()) }, - ), ( // No drain on this side: the whole boot's records are on the stick, so // the probe's line is either in them or it never ran. "sysret_ss_reload", metal::Metal { arms: SELFTESTS, judge: |b| sysret_ss(b[0].kernel().text()) }, ), - ( - "operation_nesting", - metal::Metal { - arms: SELFTESTS, - judge: |b| operation_nesting_log(b[0].kernel().text()), - }, - ), ( // A unit handed over translating, remapping and queueing, or on an // AMD-Vi machine blocking every DMA with its command buffer and event @@ -1176,27 +1153,27 @@ const METAL: &[(&str, metal::Metal)] = &[ // through a domain of its own: the register sequence, the device // table and the host tables on real silicon. The AMD bench's row. "amdvi_translates_the_stick_controller", - metal::Metal { arms: TESTCASES, judge: |b| amdvi_translates(&b[0].kernel()) }, + metal::Metal { arms: TESTCASES, judge: |b| amdvi_translates(&b[0].kernel(), &b[0].log()) }, ), ( // An AMD-Vi unit remapping every message: the boot stick's // controller's interrupt delivered through an entry of its own // table, on real silicon, and every I/O APIC named. "amdvi_remaps_the_stick_controller", - metal::Metal { arms: TESTCASES, judge: |b| amdvi_remaps(&b[0].kernel()) }, + metal::Metal { arms: TESTCASES, judge: |b| amdvi_remaps(&b[0].kernel(), &b[0].log()) }, ), ( - // The AMD bench's Bluetooth controller's xHCI, `03:00.4`, left to a - // claim and driven by usbd through an entry of its own table, its + // The AMD bench's Bluetooth controller's xHCI, `03:00.4`, which no + // row of the image names, claimed and driven by usbd through an entry of its own table, its // MSI-X table in the register BAR it hands over; then aimed past its // grant, recorded against the claim; then its holder rewriting its // own table entry to a vector not its own, refused and recorded. "amdvi_usbd_drives_its_own_entry", metal::Metal { arms: &[metal::once( - "testcases-amd-xhci-leave", + "testcases", "tests/images/testcases.toml", - &["xhci-leave=03:00.4"], + &[], &[ "test_rs_usbd_spare", "test_rs_usbd_spare_fault", @@ -1212,38 +1189,6 @@ const METAL: &[(&str, metal::Metal)] = &[ }, }, ), - ( - // The AMD bench with its unit remapping nothing (`iommu-no-remap`): - // the claim on `03:00.4` is refused before anything on it changes. - "amdvi_claim_refused_without_remapping", - metal::Metal { - arms: &[metal::once( - "testcases-amd-no-remap", - "tests/images/testcases.toml", - &["iommu-no-remap", "xhci-leave=03:00.4"], - &["test_rs_usbd_spare_unhanded"], - )], - judge: |b| { - let kernel = b[0].kernel(); - kernel.must_say("no message is remapped")?; - claims::refused_at(&kernel, AMD_XHCI_SPARE, claims::NOT_REMAPPED) - }, - }, - ), - ( - // A machine whose units do not remap: the claim is refused before - // anything on the function changes, and the boot goes on. - "claim_refused_without_remapping", - metal::Metal { - arms: &[metal::once( - "iommu-no-remap", - "tests/images/testcases.toml", - &["iommu-no-remap"], - &[claims::RECLAIM], - )], - judge: |b| claims::refused_unremapped(&b[0].kernel()), - }, - ), ( // Each radio and disk P1 names (`issues/there-is-no-wifi.md`) that the // machine has, claimed with its BARs where firmware put them, and the @@ -1263,7 +1208,28 @@ const METAL: &[(&str, metal::Metal)] = &[ }, ), ( - // The Type-C controller left to a claim, and usbd driving it: handed + // The boot controller handed over to the image's usbd row (the T14's + // 00:14.0, the AMD bench's 03:00.3), its message through its remapping + // entry, its No-Op answered by that interrupt, and the boot stick + // served through it: what every metal boot's `/log` now rides. + // `Boot: complete` and the log volume's mount are printed, not judged. + "usbd_serves_the_boot_stick", + metal::Metal { arms: TESTCASES, judge: |b| usbd_serves_the_boot_stick(&b[0].kernel(), &b[0].log()) }, + ), + ( + // The stick written through usbd while the boot deadline resets the + // machine, which touches no USB register (#901 §6): the stick must + // enumerate on the next host within the bench's record. It is the + // kernel's reset path that is judged; usbd and the job are the load. + "stick_survives_a_reset_under_load", + metal::Metal { + arms: &[metal::once("usbload", "tests/images/testcases.toml", &[], &[metal::STICK_LOAD_JOB])], + judge: |b| power::stick_survives_a_reset_under_load(b[0]), + }, + ), + ( + // The Type-C controller, which no row of the image names, claimed, + // and usbd driving it: handed // over, MSI armed through remapping, and the No-Op answered by an // interrupt; usbd is killed, so the release says which reset the // function advertises. Then a holder of it is killed and the claim @@ -1271,9 +1237,9 @@ const METAL: &[(&str, metal::Metal)] = &[ "usbd_drives_the_type_c_controller", metal::Metal { arms: &[metal::once( - "testcases-xhci-leave", + "testcases", "tests/images/testcases.toml", - &["xhci-leave=00:0d.0"], + &[], &["test_rs_usbd_spare", "test_rs_end_returns_claims"], )], judge: |b| { @@ -1393,6 +1359,7 @@ const TESTCASES: &[metal::Arm] = &[metal::once( "test_rs_wake_onto_busy", "test_rs_audio_tone", "test_rs_audio_tone_resampled", + audio::TONE_BESIDE_STICK_WRITE, "test_rs_volume_step", "test_rs_syscall_cost", "test_rs_spawn_cost", @@ -1429,30 +1396,21 @@ const WINDOWSCASE: &[metal::Arm] = &[metal::Arm { "windowscase", "tests/images/testcases.toml", &[], - &["test_rs_idle_span", "pwd", WINDOWS_LOAD], + &["test_rs_idle_span", "pwd", WINDOWS_LOAD, STICK_WRITE], ) }]; /// The load `mask_windows` reads the windows under. const WINDOWS_LOAD: &str = "test_rs_ring_park_herd"; -/// The head of the kernel's record of [`WINDOWS_LOAD`]'s exit. -fn windows_load_exited() -> String { - format!("{}{} pid=", toyos_build::bootlog::EXIT, toyos_build::bootlog::recorded_name(WINDOWS_LOAD)) -} +/// The stick-write load it reads them under too (#901 §9): 32 MiB through +/// usbd, whose interrupts and threads are a claim's and a process's. +const STICK_WRITE: &str = "test_rs_stick_write"; -/// The two boots the reset ruling is judged on: the device boot for a reset -/// 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, - devices::CONFIG, - &[], - devices::JOBS, - ), - metal::once("metalcase", "tests/images/metalcase.toml", &[], &[]), -]; +/// The head of the kernel's record of `job`'s exit. +fn exited(job: &str) -> String { + format!("{}{} pid=", toyos_build::bootlog::EXIT, toyos_build::bootlog::recorded_name(job)) +} /// `tests/images/metalcase.toml`, ended once ps2server's probe has: a boot /// that ended sooner would cut off what `ps2server_arms` reads. @@ -1491,10 +1449,7 @@ const SELFTEST_PARAMS: &[&str] = &[ "revoked-backing-selftest", "lapic-spurious-selftest", "unclaimed-vector-selftest", - "xhci-xecp-selftest", - "xhci-descriptor-selftest", "sysret-ss-probe", - "sched-operation-nesting", "iommu-firmware-left", ]; @@ -2406,6 +2361,113 @@ fn console_image_boots() -> Result<(), String> { said.must_be_clean() } +/// Boot `tests/images/testcases.toml` from a USB stick, as a metal boot does, +/// write the stick through usbd ([`metal::STICK_LOAD_JOB`]), press the power +/// button while it writes, and read the stick's log volume back with the metal +/// judge's verdict: this boot's `logkeeper` files hold `Boot: complete` and the +/// supervisor's stop line, so the log went through usbd's disk to the stick and +/// was made durable before the power went. Then usbd handed the stick back +/// with a port reset and left its controller reset, before the kernel's stop +/// ([`usb_stopped_before_the_reset`]). +fn the_log_lands_on_the_boot_stick(test_config: &Path) -> Result<(), String> { + let options = BootOptions { + profile: qemu::Profile::Metal, + qmp: true, + extra_root_files: vec![suite_bin(toyos_build::arch::Arch::X86_64, "stick_load")], + ..Default::default() + }; + let mut qemu = QemuInstance::boot_with_options(test_config, &[], &[], options); + let mut console = qemu.boot_log().to_string(); + qemu::await_marker(&mut qemu, &mut console, ": serving it as disk 0", "usbd to serve the boot stick")?; + qemu::await_marker(&mut qemu, &mut console, toyos_logstream::LOG_OPENED, "logkeeper to open the log")?; + // The stop is asked for under load: the stick is being written when the + // power button is pressed, as the metal row's deadline finds it. + writeln!(qemu.stdin_mut(), "run {}", metal::STICK_LOAD_JOB).map_err(|e| e.to_string())?; + qemu.flush_stdin(); + qemu::await_marker(&mut qemu, &mut console, "stick_load: line 1500 ", "the load to write the stick")?; + qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(qemu::GUEST_QUIET)).power_button(); + let by = qemu.budget(Duration::from_secs(60)); + console.push_str(&qemu.await_exit(by)?); + let mut stick = std::fs::File::open(qemu.boot_image()).map_err(|e| e.to_string())?; + let files = toyos_build::image::log_volume_files(&mut stick)?; + let names: Vec<&str> = files.iter().map(|(name, _)| name.as_str()).collect(); + let listing = names.join("\n"); + let (_, ours) = bootlog::split_listing(&listing); + let log: String = files + .iter() + .filter(|(name, _)| ours.contains(&name.as_str())) + .map(|(_, bytes)| String::from_utf8_lossy(bytes).into_owned()) + .collect(); + let reached = metal::bytes_of_lines(&log, "stick_load: line "); + if reached < 1 << 20 { + return Err(format!("{reached} bytes of the load's lines are on the stick's log volume, under a mebibyte")); + } + if let Err(unfit) = bootlog::verdict(&log) { + return Err(format!( + "the stick's log volume ({names:?}) is no whole boot's log: {unfit:?}\n--- its log:\n{log}\n--- the console:\n{console}" + )); + } + eprintln!(" [log] {} file(s) on the stick, {} bytes of this boot's log", names.len(), log.len()); + usb_stopped_before_the_reset(&log, &console) +} + +/// usbd ended for the stop with every connected port's reset finished and its +/// controller halted and reset (its exit code, which the kernel records, +/// because its own line may reach no log), and it ended before the kernel's +/// stop: the supervisor waited for it. +fn usb_stopped_before_the_reset(log: &str, console: &str) -> Result<(), String> { + // On the boot's one clock: the request is in the file (its line can be + // one the console dropped under the load), and what follows the flush + // is the kernel's, on the console. + use toyos_logstream::Source; + let at = |text: &str, from: Source, opens: &str| { + text.lines() + .find_map(|l| toyos_logstream::parse(l).filter(|p| p.source == from && p.text.starts_with(opens))) + .map(|p| (p.ms, p.text.to_string())) + }; + let asked = at(log, Source::Program("supervisor"), "supervisor: power: the machine stops"); + let ended = at(console, Source::Kernel, "exit: usbd pid="); + let stopped = at(console, Source::Kernel, toyos_quiesce::STOPPED); + let (Some((Some(asked), _)), Some((Some(ended), line)), Some((Some(stopped), _))) = (asked, ended, stopped) else { + return Err(format!("no stop request in the file, or no usbd end and kernel stop on the console\n{console}")); + }; + if !(asked <= ended && ended <= stopped) { + return Err(format!("usbd ended at {ended} ms, not between the stop's request ({asked} ms) and the kernel's stop ({stopped} ms)")); + } + if !line.contains(" code=0 ") { + return Err(format!("usbd's stop left a port in reset or its controller running: {line}")); + } + eprintln!(" [stop] {}", line.trim()); + Ok(()) +} + +/// Boot `images/console.toml` with a `usb-kbd` and type `ls` and Enter at it +/// through QMP, which sends every key to that keyboard: the console's shell +/// runs it, so usbd bound the keyboard, read its reports and gave the owner +/// of input its keys. +fn a_usb_key_reaches_the_console() -> Result<(), String> { + let mut qemu = QemuInstance::boot_with_options( + &compile::repo_root().join("images/console.toml"), + &[], + &[], + BootOptions { profile: qemu::Profile::MetalUsbKeyboard, qmp: true, ready_marker: bootlog::COMPLETE, ..Default::default() }, + ); + let mut console = format!("{}\n", qemu.boot_log()); + await_marker(&mut qemu, &mut console, "console: ready ", "the console to take its panel")?; + await_marker(&mut qemu, &mut console, "collection 0: a Keyboard", "usbd to bind the keyboard")?; + { + let mut input = qemu::QmpInput::open(qemu.qmp_socket()); + for key in ["l", "s", "ret"] { + input.keys(&[(key, true)]); + input.keys(&[(key, false)]); + } + } + await_guest(&mut qemu, &mut console, "the shell to run what was typed", |c| { + c.lines().any(|l| l.contains("spawn: ") && l.contains("toybox")) + })?; + serial::Serial::named("the typed-on console boot", console).must_be_clean() +} + /// Boot `tests/images/panelcase.toml` on [`qemu::Profile::MetalNoX2apic`]'s CPUs, which /// offer no x2APIC, and wait for its compositor to hold the panel: every CPU /// declared its local APIC in xAPIC mode, every CPU the firmware named came @@ -3955,6 +4017,8 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "bar_map_behind_a_bridge" => bar_map_behind_a_bridge(test_config), "lent_device_memory_refused" => lent_device_memory_refused(test_config), "console_image_boots" => console_image_boots(), + "the_log_lands_on_the_boot_stick" => the_log_lands_on_the_boot_stick(test_config), + "a_usb_key_reaches_the_console" => a_usb_key_reaches_the_console(), "desktop_without_x2apic" => desktop_without_x2apic(), "nvme_disk_boots" => nvme_disk_boots(test_config), "hosted_clang_hello" => common::hostedclang::hosted_clang_hello(), @@ -3977,7 +4041,6 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "hda_plays_to_the_controller_that_reaches_an_output" => { hda_plays_to_the_controller_that_reaches_an_output(test_config) } - "usb_keyboard_rollover" => usb_keyboard_rollover(test_config), "vector_state_survives_a_switch" => vector_state_survives_a_switch(test_config), "the_opening_exchange_with_qemus_agent_completes" => the_opening_exchange_with_qemus_agent_completes(), "crypto_instrument_prints_its_table" => crypto_instrument_prints_its_table(test_config), @@ -4188,15 +4251,12 @@ fn job_window(qemu: &mut QemuInstance, command: &str) -> Result Ok(result.serial) } -/// The AMD bench's Bluetooth controller's xHCI, the one `xhci-leave=` leaves -/// to a claim there: the stick is on the other, `03:00.3`. +/// The AMD bench's Bluetooth controller's xHCI, which no row of the test +/// images names: the stick is on the other, `03:00.3`. const AMD_XHCI_SPARE: &str = "03:00.4"; -/// QEMU's spare controller left to a claim, by the address the profile pins. -const XHCI_LEAVE_SPARE: &str = "xhci-leave=00:10.0"; - -/// The job that kills a holder of the `xhci-leave=` controller and claims it -/// again at once, in `usbd_spare`'s boots. +/// The job that kills a holder of the spare and claims it again at once, in +/// `usbd_spare`'s boots. const END_RETURNS_CLAIMS: &str = "end_returns_claims"; /// `end_returns_claims`'s summary: every round's claim granted after its @@ -4209,55 +4269,47 @@ fn ends_returned_claims(said: &str) -> Result { .ok_or_else(|| format!("end_returns_claims never said every claim came back:\n{said}")) } -/// What usbd says once the bring-up's No-Op has completed: its ring and the -/// interrupt through the claim both work. -const USBD_NOOP: &str = "usbd: a No-Op command completed, announced by an interrupt"; +/// What usbd on the controller at `at` says once the bring-up's No-Op has +/// completed: its ring and the interrupt through the claim both work, since +/// usbd reads its event ring only when an interrupt record says to. +fn usbd_noop(at: &str) -> String { + format!("usbd {at}: a No-Op command completed") +} -/// The controller `xhci-leave=` names, handed over to `usbd_spare`'s claim and +/// The spare no `drives` row names, handed over to `usbd_spare`'s claim and /// driven by usbd as far as its No-Op, answered by an interrupt through the /// claim, with every event announced by one. Then the two controls: the /// function aimed past every grant is the kernel's `DMA FAULT` record against -/// its slot and the claim's refusal, and on a kernel without the switch the -/// claim is refused because the kernel drives the function. Between them, a -/// holder of the function is killed and the claim asked for again at once, -/// each round, which only a release finished before the end grants. +/// its slot and the claim's refusal, and a claim on the boot controller, which +/// the image's usbd holds, is refused as held. Between them, a holder of the +/// spare is killed and the claim asked for again at once, each round, which +/// only a release finished before the end grants. fn usbd_drives_the_spare(test_config: &Path) -> Result<(), String> { const JOB: &str = "usbd_spare"; const SPARE: &str = "1b36:000d"; + let at = qemu::SPARE_AT; let tests = compile::repo_root().join("tests/guest/rust-tests"); let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &tests, JOB); let ends = qemu::build_toyos_bin(qemu::SUITE_ARCH, &tests, END_RETURNS_CLAIMS); - let profile = qemu::Profile::HeadlessUsbSpare; - let options = BootOptions { - profile, - kernel_params: &[XHCI_LEAVE_SPARE], - ..Default::default() - }; + let options = BootOptions { profile: qemu::Profile::HeadlessUsbSpare, ..Default::default() }; let mut qemu = QemuInstance::boot_with_options( test_config, &[], - &[(JOB.to_string(), bin.clone()), (END_RETURNS_CLAIMS.to_string(), ends)], + &[(JOB.to_string(), bin), (END_RETURNS_CLAIMS.to_string(), ends)], options, ); - let boot = serial::Serial::boot(&qemu); - boot.must_be_clean()?; - let left = format!(" {SPARE} to a claim (xhci-leave)"); - let at = boot - .text() - .lines() - .find_map(|l| l.split_once("xHCI: leaving PCI ")?.1.strip_suffix(left.as_str()).map(str::to_string)) - .ok_or_else(|| format!("the kernel never said it left {SPARE} alone:\n{}", boot.text()))?; + serial::Serial::boot(&qemu).must_be_clean()?; let said = job_window(&mut qemu, "test_rs_usbd_spare")?; let run = serial::Serial::named("usbd_spare", said.clone()); run.must_be_clean()?; - run.must_say(USBD_NOOP)?; + run.must_say(&usbd_noop(at))?; run.must_say("usbd_spare: settled: usb.events.unannounced = 0")?; let handed = format!("pcidev: PCI {at} [{SPARE}] handed over, vector "); if said.matches(&handed).count() != 1 { return Err(format!("{at} was not handed over once, to the one usbd:\n{said}")); } - run.must_say(&format!("PCI {at}: msi address="))?; + run.must_say(&format!("PCI {at}: msix address="))?; let faulted = job_window(&mut qemu, "test_rs_usbd_spare fault")?; let fault = serial::Serial::named("usbd_spare fault", faulted.clone()); @@ -4279,20 +4331,10 @@ fn usbd_drives_the_spare(test_config: &Path) -> Result<(), String> { let ended = job_window(&mut qemu, "test_rs_end_returns_claims")?; let line = ends_returned_claims(&ended)?; eprintln!(" [usbd] {line}"); - drop(qemu); - // The control: the same machine and no switch, so the kernel drives it. - let mut qemu = QemuInstance::boot_with_options( - test_config, - &[], - &[(JOB.to_string(), bin)], - BootOptions { profile, ..Default::default() }, - ); - serial::Serial::boot(&qemu).must_not_say("(xhci-leave)")?; + // The control: the boot controller, which the image's usbd holds. let said = job_window(&mut qemu, "test_rs_usbd_spare refused")?; - let refused = serial::Serial::named("usbd_spare refused", said); - refused.must_say(&format!("usbd_spare: the claim on pci:{SPARE} was refused: PermissionDenied"))?; - refused.must_say(&format!("pcidev: PCI {at} is driven by this kernel and cannot be claimed"))?; + serial::Serial::named("usbd_spare refused", said).must_say("usbd_spare: the claim on pci:1033:0194 was refused: AlreadyExists")?; Ok(()) } @@ -4301,7 +4343,7 @@ fn usbd_drives_the_spare(test_config: &Path) -> Result<(), String> { fn usbd_refused_on_amdvi(test_config: &Path, profile: qemu::Profile, said: &str, why: &str) -> Result<(), String> { const JOB: &str = "usbd_spare"; let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &compile::repo_root().join("tests/guest/rust-tests"), JOB); - let options = BootOptions { profile, kernel_params: &[XHCI_LEAVE_SPARE], ..Default::default() }; + let options = BootOptions { profile, ..Default::default() }; let mut qemu = QemuInstance::boot_with_options(test_config, &[], &[(JOB.to_string(), bin)], options); serial::Serial::boot(&qemu).must_say(said)?; let run = job_window(&mut qemu, "test_rs_usbd_spare unhanded")?; @@ -4359,7 +4401,6 @@ fn a_drives_row_holds_one_process_per_function() -> Result<(), String> { fn amdvi_usbd(kernel: &serial::Serial, log: &serial::Serial, after: &serial::Serial) -> Result<(), String> { const ID: &str = "1022:1639"; let at = AMD_XHCI_SPARE; - kernel.must_say(&format!("xHCI: leaving PCI {at} {ID} to a claim (xhci-leave)"))?; let handed = format!("pcidev: PCI {at} [{ID}] handed over, vector "); let vector = kernel .text() @@ -4368,7 +4409,7 @@ fn amdvi_usbd(kernel: &serial::Serial, log: &serial::Serial, after: &serial::Ser .ok_or_else(|| format!("{at} was never handed over:\n{}", kernel.text()))?; kernel.must_say(&format!("PCI {at}: msix address="))?; kernel.must_say(&format!("iommu: amdvi irte source={at} vector={vector} p=1 "))?; - log.must_say(USBD_NOOP)?; + log.must_say(&usbd_noop(at))?; let aimed = log .text() @@ -4376,19 +4417,19 @@ fn amdvi_usbd(kernel: &serial::Serial, log: &serial::Serial, after: &serial::Ser .find_map(|l| l.split_once("usbd_spare: the command ring aimed at ")?.1.split_once(',').map(|(a, _)| a.to_string())) .ok_or_else(|| format!("the fault arm never said where it aimed:\n{}", log.text()))?; let hex = |text: &str| u64::from_str_radix(text.trim_start_matches("0x"), 16).ok(); - let records: Vec<&str> = kernel - .text() - .lines() - .filter(|l| l.contains("iommu: DMA FAULT owner=process") && l.contains(&format!("stream={at} "))) - .collect(); + // Every record of the spare's: the boot's other rows leave these to this + // one ([`no_fault_but_the_spares`]), so each must be one the jobs staged. + let records: Vec<&str> = + kernel.text().lines().filter(|l| l.contains("iommu: DMA FAULT") && l.contains(&format!(" stream={at} "))).collect(); + let process = |l: &&&str| l.contains("iommu: DMA FAULT owner=process "); let aimed_at = records .iter() - .find(|l| l.split_once(" addr=").and_then(|(_, a)| hex(a.split_whitespace().next()?)) == hex(&aimed)) + .find(|l| process(l) && l.contains(" access=read ") && l.split_once(" addr=").and_then(|(_, a)| hex(a.split_whitespace().next()?)) == hex(&aimed)) .ok_or_else(|| format!("no record of {at} names the aimed {aimed}: {records:?}"))?; log.must_say("usbd_spare: the claim refuses its interrupt read: Io")?; let rewritten = log.must_say("usbd_spare: MSI-X entry 0's data ")?; - log.must_say("usbd: the claim refused its interrupt record (Io)")?; + log.must_say(&format!("usbd {at}: ENDED — the claim refused its interrupt record (Io)"))?; log.must_say("usbd_spare: usbd ended on the claim's refusal")?; // Refused by the unit, not delivered to a vector nobody holds: the stop's // census names every vector the unclaimed gate took. @@ -4404,8 +4445,11 @@ fn amdvi_usbd(kernel: &serial::Serial, log: &serial::Serial, after: &serial::Ser // An interrupt's record states no read or write (48882 Table 57, `I`). let foreign = records .iter() - .find(|l| l.contains(" access=none ") && l.ends_with("IO_PAGE_FAULT")) + .find(|l| process(l) && l.contains(" access=none ") && l.ends_with("IO_PAGE_FAULT")) .ok_or_else(|| format!("no record of {at}'s message on a vector not its own: {records:?}"))?; + if records.len() != 2 { + return Err(format!("{at} has {} fault records, not the aimed read and the foreign message alone: {records:?}", records.len())); + } eprintln!(" [amdvi] {at}: handed over on {vector}, its No-Op answered through its own entry"); eprintln!(" [amdvi] aimed at {aimed}: {}", aimed_at.trim()); eprintln!(" [amdvi] a vector not its own: {}", foreign.trim()); @@ -4459,10 +4503,40 @@ fn hda_plays_to_the_controller_that_reaches_an_output(test_config: &Path) -> Res Ok(()) } -/// Every AMD-Vi unit translating, every xHCI controller the kernel drives -/// moved to a domain of its own, the boot stick bound through one, and no -/// DMA refused. -fn amdvi_translates(kernel: &serial::Serial) -> Result<(), String> { +/// Every xHCI controller handed over to a usbd: by the kernel's hand-over +/// record, of the ids the test images' usbd rows and spares name. +fn usb_controllers(kernel: &serial::Serial) -> Vec { + const XHCI: [&str; 5] = ["1033:0194", "8086:a0ed", "8086:9a13", "1022:1639", "1b36:000d"]; + kernel + .text() + .lines() + .filter_map(|l| { + let (at, rest) = l.split_once("pcidev: PCI ")?.1.split_once(" [")?; + let (id, rest) = rest.split_once(']')?; + (XHCI.contains(&id) && rest.starts_with(" handed over")).then(|| at.to_string()) + }) + .collect() +} + +/// No DMA refused on the AMD bench's testcases boot but the spare's: that boot +/// also carries `amdvi_usbd_drives_its_own_entry`, whose jobs aim +/// [`AMD_XHCI_SPARE`] past its grant and at a vector not its own, and which +/// judges those records itself. +fn no_fault_but_the_spares(kernel: &serial::Serial) -> Result<(), String> { + kernel.alive()?; + let spares = format!(" stream={AMD_XHCI_SPARE} "); + match kernel.text().lines().find(|l| l.contains("iommu: DMA FAULT") && !l.contains(&spares)) { + Some(line) => Err(format!("a DMA refusal no job asked for: {line:?}")), + None => Ok(()), + } +} + +/// usbd's word that it serves the boot stick: its first disk offered. +const STICK_SERVED: &str = ": serving it as disk 0"; + +/// Every AMD-Vi unit translating, every xHCI controller usbd drives on a +/// domain of its own, the boot stick served through one, and no DMA refused. +fn amdvi_translates(kernel: &serial::Serial, log: &serial::Serial) -> Result<(), String> { let text = kernel.text(); let units: Vec<&str> = text.lines().filter(|l| l.contains("iommu: amdvi unit") && l.contains(" translating control=")).collect(); @@ -4470,32 +4544,29 @@ fn amdvi_translates(kernel: &serial::Serial) -> Result<(), String> { return Err(format!("no AMD-Vi unit is translating:\n{text}")); } kernel.must_not_say("isolates no device")?; - kernel.must_not_say("iommu: DMA FAULT")?; - let controllers: Vec<&str> = text - .lines() - .filter_map(|l| l.split_once("xHCI: found at PCI ")?.1.split_whitespace().next()) - .collect(); + no_fault_but_the_spares(kernel)?; + let controllers = usb_controllers(kernel); if controllers.is_empty() { - return Err(format!("the kernel drove no xHCI controller:\n{text}")); + return Err(format!("no xHCI controller was handed over:\n{text}")); } for at in &controllers { kernel.must_say(&format!("iommu: {at} moves to domain"))?; } - kernel.must_say("usb-storage: 1 device(s)")?; + log.must_say(STICK_SERVED)?; for unit in units { eprintln!(" [amdvi] {}", unit.trim()); } - eprintln!(" [amdvi] {} controller(s) on domains of their own, the stick bound", controllers.len()); + eprintln!(" [amdvi] {} controller(s) on domains of their own, the stick served", controllers.len()); Ok(()) } /// Every AMD-Vi unit remapping (`GAEn`, MMIO 0018h bit 17) and, once the /// kernel's drivers are settled, filtering SMIs and logging each it refuses /// (`SmiFEn` bit 22, `SmiFLogEn` bit 24); every I/O APIC the MADT has named -/// by the IVRS; and each xHCI controller the kernel drives armed through a -/// present entry of its own table at the vector its message carries, the -/// stick bound by those interrupts. -fn amdvi_remaps(kernel: &serial::Serial) -> Result<(), String> { +/// by the IVRS; and each xHCI controller usbd drives armed through a present +/// entry of its own table at the vector its message carries, the stick served +/// by those interrupts. +fn amdvi_remaps(kernel: &serial::Serial, log: &serial::Serial) -> Result<(), String> { let text = kernel.text(); let remaps = kernel.must_say("iommu: amdvi remaps every message through its requester's table")?; let control = |line: &str| { @@ -4526,11 +4597,8 @@ fn amdvi_remaps(kernel: &serial::Serial) -> Result<(), String> { if !remaps.contains(&format!(" {apics} I/O APIC(s) named")) { return Err(format!("{remaps:?} does not name the {apics} I/O APIC(s) the MADT has")); } - kernel.must_not_say("iommu: DMA FAULT")?; - let controllers: Vec<&str> = text - .lines() - .filter_map(|l| l.split_once("xHCI: found at PCI ")?.1.split_whitespace().next()) - .collect(); + no_fault_but_the_spares(kernel)?; + let controllers = usb_controllers(kernel); for at in &controllers { let data = text .lines() @@ -4539,7 +4607,7 @@ fn amdvi_remaps(kernel: &serial::Serial) -> Result<(), String> { let vector = u32::from_str_radix(data.trim_start_matches("0x"), 16).map_err(|e| format!("{data}: {e}"))? & 0xFF; kernel.must_say(&format!("iommu: amdvi irte source={at} vector={vector:#04x} p=1 "))?; } - kernel.must_say("usb-storage: 1 device(s)")?; + log.must_say(STICK_SERVED)?; eprintln!(" [amdvi] {}", remaps.trim()); for filter in filters { eprintln!(" [amdvi] {}", filter.trim()); @@ -4547,7 +4615,44 @@ fn amdvi_remaps(kernel: &serial::Serial) -> Result<(), String> { for refused in text.lines().filter(|l| l.contains("iommu: amdvi unit") && l.contains(" said so far")) { eprintln!(" [amdvi] {}", refused.trim()); } - eprintln!(" [amdvi] {} controller(s) armed through their own entries, the stick bound", controllers.len()); + eprintln!(" [amdvi] {} controller(s) armed through their own entries, the stick served", controllers.len()); + Ok(()) +} + +/// The controller the boot stick is on, handed over once to the image's +/// usbd, its message armed in remappable format through an entry of its own +/// (VT-d's Interrupt Format, bit 4 of an MSI address; AMD-Vi's entry for its +/// source), its No-Op answered, and the stick served by it. +fn usbd_serves_the_boot_stick(kernel: &serial::Serial, log: &serial::Serial) -> Result<(), String> { + let served: Vec = usb_controllers(kernel) + .into_iter() + .filter(|at| log.text().lines().any(|l| l.contains(&format!("usbd {at}: ")) && l.ends_with(STICK_SERVED))) + .collect(); + let [at] = &served[..] else { + return Err(format!("not one controller served the stick: {served:?}\n{}", log.text())); + }; + let handed = format!("pcidev: PCI {at} ["); + let handovers: Vec<&str> = + kernel.text().lines().filter(|l| l.contains(&handed) && l.contains("] handed over")).collect(); + let [line] = handovers[..] else { + return Err(format!("{at} was not handed over once: {handovers:?}")); + }; + let remapped = kernel.text().lines().any(|l| { + l.split_once(&format!("PCI {at}: msi address=")) + .and_then(|(_, a)| u32::from_str_radix(a.split_whitespace().next()?.trim_start_matches("0x"), 16).ok()) + .is_some_and(|address| address & (1 << 4) != 0) + }) || kernel.text().contains(&format!("iommu: amdvi irte source={at} ")); + if !remapped { + return Err(format!("{at}'s message names no remapping entry of its own:\n{}", kernel.text())); + } + log.must_say(&usbd_noop(at))?; + eprintln!(" [usbd] {}", line.trim()); + for complete in kernel.text().lines().filter(|l| l.contains("Boot: complete")) { + eprintln!(" [usbd] {}", complete.trim()); + } + if let Some(mounted) = log.text().lines().find(|l| l.contains("fileserver: Log serving /log")) { + eprintln!(" [usbd] {}", mounted.trim()); + } Ok(()) } @@ -4558,12 +4663,12 @@ fn amdvi_remaps(kernel: &serial::Serial) -> Result<(), String> { /// release and printed, not judged. fn usbd_on_metal(kernel: &serial::Serial, log: &serial::Serial) -> Result<(), String> { const TYPE_C: &str = "8086:9a13"; + let handed = format!(" [{TYPE_C}] handed over, vector "); let at = kernel .text() .lines() - .find_map(|l| l.split_once("xHCI: leaving PCI ")?.1.strip_suffix(&format!(" {TYPE_C} to a claim (xhci-leave)")).map(str::to_string)) - .ok_or_else(|| format!("the kernel never said it left {TYPE_C} alone:\n{}", kernel.text()))?; - kernel.must_say(&format!("pcidev: PCI {at} [{TYPE_C}] handed over, vector "))?; + .find_map(|l| l.split_once("pcidev: PCI ")?.1.split_once(&handed).map(|(at, _)| at.to_string())) + .ok_or_else(|| format!("{TYPE_C} was never handed over:\n{}", kernel.text()))?; let message = kernel .text() .lines() @@ -4577,7 +4682,7 @@ fn usbd_on_metal(kernel: &serial::Serial, log: &serial::Serial) -> Result<(), St } kernel.must_say(&format!(" source={at} p=1 "))?; kernel.must_say("took its first message on vector")?; - log.must_say(USBD_NOOP)?; + log.must_say(&usbd_noop(&at))?; for released in kernel.text().lines().filter(|l| l.contains(&format!("pcidev: PCI {at} [{TYPE_C}] released; "))) { eprintln!(" [usbd] {}", released.trim()); } @@ -4585,34 +4690,6 @@ fn usbd_on_metal(kernel: &serial::Serial, log: &serial::Serial) -> Result<(), St Ok(()) } -/// Seven keys held on QEMU's `usb-kbd`, whose seventh report is ErrorRollOver -/// in every slot: the driver counts it as a rollover, and as no refusal. -fn usb_keyboard_rollover(test_config: &Path) -> Result<(), String> { - const BOUND: &str = "xHCI: USB keyboard ready on slot "; - const ROLLED_OVER: &str = " keyboard report in rollover, its modifiers believed and its other keys held, 1 since it bound"; - const REFUSED: &str = " report refused, "; - let options = BootOptions { qmp: true, ..Default::default() }; - if !qemu::profile_argv(&options).iter().any(|arg| arg.starts_with("usb-kbd,")) { - return Err("this machine was to have a usb-kbd".to_string()); - } - let mut qemu = QemuInstance::boot_with_options(test_config, &[], &[], options); - let mut console = format!("{}\n", qemu.boot_log()); - await_guest(&mut qemu, &mut console, "the keyboard to bind", |c| c.contains(BOUND))?; - const KEYS: [&str; 7] = ["q", "w", "e", "r", "t", "y", "u"]; - { - let mut input = qemu::QmpInput::open(qemu.qmp_socket()); - input.keys(&KEYS.map(|key| (key, true))); - input.keys(&KEYS.map(|key| (key, false))); - } - await_guest(&mut qemu, &mut console, "the driver to read the rollover", |c| { - c.contains(ROLLED_OVER) || c.contains(REFUSED) - })?; - let said = serial::Serial::named("the typed-on boot", console); - said.must_not_say(REFUSED)?; - eprintln!(" [usbhid] {}", said.must_say(ROLLED_OVER)?.trim()); - said.must_be_clean() -} - /// [`MACHINE_TESTS`]' `vector_state_survives_a_switch`: the shared boot's /// `vector_state` alone on two CPUs, so five threads time-share them. fn vector_state_survives_a_switch(test_config: &Path) -> Result<(), String> { @@ -4919,12 +4996,22 @@ fn windows_on_metal(boot: &metal::Readback) -> Result<(), String> { let kernel = boot.kernel(); let cpus = boot.cpus()?; mask_windows(kernel.text(), cpus)?; - let read = common::irqcensus::windows_under(kernel.text(), cpus, &windows_load_exited())?; + let read = common::irqcensus::windows_under(kernel.text(), cpus, &exited(WINDOWS_LOAD))?; eprintln!( " [windows] held irqs_off_ns={} preempt_off_ns={}; herd irqs_off_ns={} preempt_off_ns={}", read.held.0, read.held.1, read.load.0, read.load.1 ); - Ok(()) + // Under the stick write, each CPU's longest windows are judged against + // the bench's record (`timings::ceiling`), the base they were first read at. + boot.job_passed(STICK_WRITE)?; + let reached = metal::bytes_of_lines(boot.log().text(), "stick_write: line "); + if reached < 1 << 20 { + return Err(format!("{reached} bytes of the stick write's lines reached the stick, under a mebibyte")); + } + let under = common::irqcensus::windows_under(kernel.text(), cpus, &exited(STICK_WRITE))?; + eprintln!(" [windows] stick write irqs_off_ns={} preempt_off_ns={}", under.load.0, under.load.1); + boot.measured("boot.windowscase.stick_write.irqs_off_ns", under.load.0)?; + boot.measured("boot.windowscase.stick_write.preempt_off_ns", under.load.1) } fn pci_cap_selftest(log: &str) -> Result<(), String> { @@ -5053,96 +5140,6 @@ fn lapic_vectors(log: &str) -> Result<(), String> { Ok(()) } -/// Nine crafted USB configuration descriptors, parsed at init. -fn xhci_descriptors(log: &str) -> Result<(), String> { - if let Some(bad) = log.lines().find(|l| l.contains("descriptor selftest FAILED")) { - return Err(format!("{bad}\n{log}")); - } - let Some(verdict) = log.lines().find(|l| l.contains("descriptor selftest")) else { - return Err(format!("the parser's self-test never ran:\n{log}")); - }; - // `9/9`, not "no failures": a self-test that ran zero cases would - // satisfy the absence of a FAILED line. - if !verdict.contains("9/9") { - return Err(format!("not every descriptor was parsed as required: {verdict}")); - } - // Once for the machine. It reads no register, so a per-controller - // run would be two verdicts about the same nine byte arrays. - let ran = log.matches("descriptor selftest").count(); - if ran != 1 { - return Err(format!("the self-test ran {ran} times, wanted once\n{log}")); - } - // And the ordinary boot beside it: the same parser bound the boot - // stick off a descriptor a real controller delivered. - if !log.contains("usb-storage: 1 device(s)") { - return Err(format!("the boot stick did not bind on this boot\n{log}")); - } - eprintln!(" [xhci] {}", verdict.trim()); - Ok(()) -} - -/// Eight malformed extended-capability lists refused, and the handoff on every controller. -fn xhci_xecp(log: &str) -> Result<(), String> { - if let Some(bad) = log.lines().find(|l| l.contains("xecp selftest FAILED")) { - return Err(format!("{bad}\n{log}")); - } - let Some(verdict) = log.lines().find(|l| l.contains("xecp selftest")) else { - return Err(format!("the walk's self-test never ran:\n{log}")); - }; - // `8/8`, not "no failures": a self-test that ran zero cases would - // satisfy the absence of a FAILED line. - if !verdict.contains("8/8") { - return Err(format!("not every malformed list was refused: {verdict}")); - } - // And the handoff on every controller, in `take_ownership`'s words for - // each outcome that leaves the kernel owning it: no capability - // (QEMU's), firmware that never claimed it (the T14's), and firmware - // that released it. Each precedes its own reset — a reset that already - // happened is what the whole capability exists to avoid. - const HANDED_OVER: &[&str] = &[ - "xHCI: no USB Legacy Support capability", - "xHCI: firmware did not claim the controller", - "xHCI: firmware released the controller", - ]; - const KEPT: &[&str] = &[ - "xHCI: extended capability list unusable", - "runs past the register window — no handoff", - "xHCI: firmware still owns the controller", - ]; - let mut handoffs = Vec::new(); - let mut pending: Option<&str> = None; - for line in log.lines() { - if KEPT.iter().any(|said| line.contains(said)) { - return Err(format!("a controller was never handed over: {line}\n{log}")); - } - if HANDED_OVER.iter().any(|said| line.contains(said)) { - if let Some(earlier) = pending.replace(line) { - return Err(format!("a handoff with no reset of its own: {earlier}\n{log}")); - } - } else if line.contains("xHCI: controller reset") { - let Some(handoff) = pending.take() else { - return Err(format!("a controller reset before its handoff: {line}\n{log}")); - }; - handoffs.push(handoff); - } - } - if let Some(unreset) = pending { - return Err(format!("a handoff with no reset of its own: {unreset}\n{log}")); - } - if handoffs.is_empty() { - return Err(format!("no controller was handed over and reset:\n{log}")); - } - // A controller that still enumerates its bus afterwards. - if !log.contains("xHCI: controller started") { - return Err(format!("the controller did not come up:\n{log}")); - } - eprintln!(" [xhci] {}", verdict.trim()); - for handoff in handoffs { - eprintln!(" [xhci] {}", handoff.trim()); - } - Ok(()) -} - /// `kernel/src/arch/x86_64/hw.rs`'s probe, when it could not run. const SYSRET_SS_UNARMED: &str = "sysret-ss: probe could not arm"; /// The probe, when a switch refreshed SS from null. @@ -5232,114 +5229,6 @@ fn iommu_firmware_left(log: &str) -> Result<(), String> { Ok(()) } -/// An inner `scheduler::Operation` may only narrow, and its drop restores what it displaced. -fn operation_nesting_log(log: &str) -> Result<(), String> { - - /// One `key=value` off a gate line, as a number. - fn number(line: &str, key: &str) -> Result { - line.split_whitespace() - .find_map(|word| word.strip_prefix(key)?.strip_prefix('=')?.parse().ok()) - .ok_or_else(|| format!("no numeric {key} in {line:?}")) - } - /// One `key=value` off a gate line, as a flag. - fn flag(line: &str, key: &str) -> Result { - line.split_whitespace() - .find_map(|word| word.strip_prefix(key)?.strip_prefix('=')?.parse().ok()) - .ok_or_else(|| format!("no boolean {key} in {line:?}")) - } - - // Both homes. A task's word is on its `TaskHandle` and a context - // with no task uses one slot per CPU, and the two are reached by - // different arms of `operation_slot` — so a gate that ran in one - // place would leave the other arm unexecuted by any test at all. - for site in ["boot", "syscall"] { - let say = |what: &str| -> Result { - let needle = format!("sched-op: {site} {what}"); - log.lines() - .find(|line| line.contains(&needle)) - .map(str::to_string) - .ok_or_else(|| format!("no {needle:?} line on this boot:\n{log}")) - }; - - let outside = say("outside")?; - if flag(&outside, "established")? { - return Err(format!( - "{site}: an operation was already established before the gate began, \ - so nothing below is about the nesting it made: {outside}" - )); - } - - // Every level: what it asked for, and what the depth below it - // recovered. The bound is the running minimum — an inner - // establishment takes the earlier of its own deadline and its - // parent's, and only that. - let mut asked = Vec::new(); - let mut narrowest = u64::MAX; - for level in 1..=3 { - let line = say(&format!("begin level={level}"))?; - let want = number(&line, "asked")?; - let saw = number(&line, "observed")?; - narrowest = narrowest.min(want); - if saw != narrowest { - return Err(format!( - "{site}: level {level} asked for {want} ns and the depth inside it \ - recovered {saw} ns, against the {narrowest} ns that is the \ - earliest of it and every level above it. An establishment that \ - observes more than its parent allowed is a caller buying itself \ - device time by nesting: {line}" - )); - } - asked.push(want); - } - // The widening attempt has to have been a real one, or the line - // above is satisfied by a scenario in which nothing was asked. - if asked[2] <= asked[1] { - return Err(format!( - "{site}: level 3 asked for {} ns inside a level 2 of {} ns, so the \ - gate never attempted to widen and the narrowing it reports is vacuous", - asked[2], asked[1], - )); - } - - // And the restore: each drop puts back what that establishment - // displaced rather than clearing the slot, so the operation - // above it survives the one below ending. - for (level, restored) in [(3, asked[1].min(asked[0])), (2, asked[0])] { - let line = say(&format!("end level={level}"))?; - let saw = number(&line, "observed")?; - if saw != restored { - return Err(format!( - "{site}: with level {level} dropped the depth recovered {saw} ns \ - and the frame above it established {restored} ns — a guard that \ - restores something else has ended an operation its caller is \ - still inside: {line}" - )); - } - if !flag(&line, "established")? { - return Err(format!( - "{site}: dropping level {level} left no operation established at \ - all, and its caller is still inside one: {line}" - )); - } - } - - let last = say("end level=1")?; - if flag(&last, "established")? { - return Err(format!( - "{site}: the outermost guard dropped and an operation is still \ - established — the slot was restored rather than cleared, so the next \ - depth to ask would be answered a deadline nobody set: {last}" - )); - } - eprintln!( - " [operation] {site}: {} ns narrowed to {} ns, a {} ns request changed \ - nothing, and both drops restored", - asked[0], asked[1], asked[2], - ); - } - Ok(()) -} - /// The machine's kernel thread is hosted. fn klogd_hosted(boot: &serial::Serial) -> Result<(), String> { boot.must_be_clean()?;