From 3a67835972e05182f3e442c03a860e2ec30c8b93 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 14 Oct 2026 17:39:52 +0200 Subject: [PATCH 1/2] virtio-gpu leaves the kernel: the compositor drives it through its claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kernel had the only virtio-gpu driver, behind a `Gpu` trait and four `SYS_GPU_*` syscalls the compositor and the console called through the framebuffer claim. The compositor now claims `pci:1af4:1050` and drives the function itself over `toyos-virtio`; the kernel keeps only the firmware's framebuffer, which a `framebuffer` claim maps as one scanout. - `system/compositor/virtio-gpu` (`toyos-virtio-gpu`): the 2D commands of Virtio 1.2 §5.7 over the control and cursor queues in one grant. Every control command is fenced and its answer held to the fence, length and type; `RESP_ERR_OUT_OF_MEMORY` to the three commands that make a scanout declines a mode, any other error ends the device. A mode change is a page flip: the new scanout is lent, created, attached and drawn while the old one is on screen, and the old backing is given back only after `SET_SCANOUT` and its fenced `UNREF`. Every wake of the claim, inside a command's wait too, drains the cursor queue and reads `events_read`; the output is read after the events are cleared. Host-tested on a model of the device. - The compositor takes virtio first and the framebuffer otherwise (`display.rs`), starts at the device's preferred size clamped into `toyos-desktop`'s mode bounds (642x446 to 7680x4320) and flips to the size each display event asks for. `screen WxH` takes the same flip. - `toyos-virtio`'s `Live` reads and writes the device-specific structure. - pcidev: what a claim lends is bounded by its own window, `toyos_abi::pci::LEND_WINDOW` (256 MiB, two scanouts at the largest mode), apart from the 32 MiB grant bound. - Gone: `drivers/virtio_gpu.rs`, `gpu.rs`, the four `SYS_GPU_*` numbers and `toyos::gpu`, the claim's cursor and second scanout, the panic console's virtio arms, `console`'s presents, `abuse_gpu_resolution`, and the decline control with its `virtio-no-access-platform` actuator, by the orchestrator's ruling on PR #895. - `cargo run`'s Virtio profile on macOS opens `cocoa,zoom-to-fit=on`, so the window's size reaches the guest. - `the_compositor_drives_the_virtio_gpu` boots QEMU's virtio-gpu, reads the first frame off the scanout, resizes the display over VNC and reads the followed mode's frame. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 11 + Cargo.toml | 1 + build/qemu.rs | 18 +- images/system.toml | 5 +- ...in-flight-acts-after-the-claims-release.md | 25 +- ...-domains-addresses-are-never-given-back.md | 12 +- .../a-lent-scanout-is-one-contiguous-run.md | 21 + ...refused-framebuffer-reads-as-no-display.md | 6 +- ...-lends-around-its-predecessors-scanouts.md | 26 + issues/every-driver-is-still-in-the-kernel.md | 47 +- .../the-desktop-draws-at-the-panels-scale.md | 11 +- ...iver-asserts-on-what-the-device-answers.md | 20 - ...second-scanout-buffer-is-read-by-nobody.md | 14 - issues/toyos-runs-on-arm64.md | 4 +- ...volatile-composites-on-mmio-dma-structs.md | 4 +- ...-region-may-be-lent-is-a-runtime-option.md | 4 +- kernel/src/actuator.rs | 3 - kernel/src/device.rs | 24 +- kernel/src/drivers/gop.rs | 72 +- kernel/src/drivers/mod.rs | 1 - kernel/src/drivers/panic_console/mod.rs | 28 +- kernel/src/drivers/virtio.rs | 23 +- kernel/src/drivers/virtio_gpu.rs | 649 --------------- kernel/src/gpu.rs | 121 --- kernel/src/main.rs | 18 +- kernel/src/object/device.rs | 48 +- kernel/src/object/shm.rs | 11 +- kernel/src/pcidev/mod.rs | 32 +- kernel/src/syscall/device.rs | 52 +- kernel/src/syscall/dispatch.rs | 44 +- lib/virtio/src/pci.rs | 43 +- lib/virtio/src/stub.rs | 11 +- lib/virtio/src/tests.rs | 24 + sdk/abi/src/lib.rs | 14 +- sdk/abi/src/pci.rs | 5 + sdk/abi/src/syscall.rs | 59 +- sdk/toyos/src/device.rs | 4 +- sdk/toyos/src/gpu.rs | 48 -- sdk/toyos/src/lib.rs | 1 - system/compositor/Cargo.toml | 7 +- system/compositor/desktop/src/lib.rs | 2 + system/compositor/desktop/src/mode.rs | 176 ++++ system/compositor/desktop/src/scale.rs | 24 +- system/compositor/src/display.rs | 396 +++++++++ system/compositor/src/main.rs | 15 +- system/compositor/src/render.rs | 28 +- system/compositor/src/session.rs | 312 ++++--- system/compositor/src/stats.rs | 11 +- system/compositor/virtio-gpu/Cargo.toml | 24 + system/compositor/virtio-gpu/src/lib.rs | 650 +++++++++++++++ system/compositor/virtio-gpu/src/tests.rs | 787 ++++++++++++++++++ system/compositor/virtio-gpu/src/wire.rs | 148 ++++ system/console/src/main.rs | 31 +- tests/common/iommu.rs | 50 +- tests/common/mod.rs | 2 + tests/common/qemu.rs | 28 +- tests/common/vnc.rs | 74 ++ tests/guest/metalprobe/src/fb.rs | 2 +- .../src/bin/abuse_gpu_resolution.rs | 127 --- tests/images/clipboardcase.toml | 11 +- tests/toyos.rs | 61 +- 61 files changed, 2821 insertions(+), 1709 deletions(-) create mode 100644 issues/a-lent-scanout-is-one-contiguous-run.md create mode 100644 issues/a-restarted-display-holder-lends-around-its-predecessors-scanouts.md delete mode 100644 issues/the-gpu-driver-asserts-on-what-the-device-answers.md delete mode 100644 issues/the-second-scanout-buffer-is-read-by-nobody.md delete mode 100644 kernel/src/drivers/virtio_gpu.rs delete mode 100644 kernel/src/gpu.rs delete mode 100644 sdk/toyos/src/gpu.rs create mode 100644 system/compositor/desktop/src/mode.rs create mode 100644 system/compositor/src/display.rs create mode 100644 system/compositor/virtio-gpu/Cargo.toml create mode 100644 system/compositor/virtio-gpu/src/lib.rs create mode 100644 system/compositor/virtio-gpu/src/tests.rs create mode 100644 system/compositor/virtio-gpu/src/wire.rs create mode 100644 tests/common/vnc.rs delete mode 100644 tests/guest/rust-tests/src/bin/abuse_gpu_resolution.rs diff --git a/Cargo.lock b/Cargo.lock index 4d7d4192dc8..7a6680bae89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -559,6 +559,9 @@ dependencies = [ "toyos-input", "toyos-inspect", "toyos-manifest", + "toyos-pci-claim", + "toyos-virtio", + "toyos-virtio-gpu", "toyos-window", ] @@ -4577,6 +4580,14 @@ dependencies = [ "toyos-untrusted", ] +[[package]] +name = "toyos-virtio-gpu" +version = "0.1.0" +dependencies = [ + "toyos-device-memory", + "toyos-virtio", +] + [[package]] name = "toyos-virtio-sound" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 266e2e06747..8d9e94dcac9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,7 @@ members = [ "system/compositor", "system/compositor/desktop", "system/compositor/sprite", + "system/compositor/virtio-gpu", "system/console", "system/diskserver", "system/filepicker", diff --git a/build/qemu.rs b/build/qemu.rs index 14f82e2e3a0..aae7578024b 100644 --- a/build/qemu.rs +++ b/build/qemu.rs @@ -130,10 +130,10 @@ pub fn launch(opts: &Options) { let arch = opts.arch; // Before anything launches: every virtio machine carries the clipboard's // channel, and every host's display is the one with its clipboard. - let display = display(); + let display = display(shape.virtio_gpu); let wanted = [ shape.virtio.then(|| toyos_build::vdagent::available(arch)), - (display != "cocoa").then(|| toyos_build::vdagent::gtk(arch)), + (!display.starts_with("cocoa")).then(|| toyos_build::vdagent::gtk(arch)), ]; if let Some(refused) = wanted.into_iter().flatten().find_map(Result::err) { eprintln!("Error: {refused}"); @@ -358,11 +358,15 @@ fn iommu_device(arch: Arch) -> Option<&'static str> { /// The display, with the host's clipboard: QEMU's own on macOS, and GTK's, /// whose clipboard QEMU leaves off unless asked, everywhere else. -fn display() -> &'static str { - if cfg!(target_os = "macos") { - "cocoa" - } else { - "gtk,clipboard=on" +/// +/// On macOS a virtio-gpu guest follows the window (`zoom-to-fit`), so a +/// resize reaches its mode; a firmware framebuffer cannot follow, and its +/// window stays 1:1 with it. GTK has virtio-gpu follow the window already. +fn display(follows: bool) -> &'static str { + match (cfg!(target_os = "macos"), follows) { + (true, true) => "cocoa,zoom-to-fit=on", + (true, false) => "cocoa", + (false, _) => "gtk,clipboard=on", } } diff --git a/images/system.toml b/images/system.toml index 740113a1c60..55db78a2cd1 100644 --- a/images/system.toml +++ b/images/system.toml @@ -76,12 +76,13 @@ receives = ["power"] # made in the session, and `vdagent` alone receives it. `input` is where a source # of keys and pointer reports reaches the owner of input, which it is. `volume` # is soundserver's master level and mute, which the volume keys set and no -# other row receives. +# other row receives. The display is a virtio-gpu function the compositor +# drives, where the machine has one, and the firmware's framebuffer otherwise. [programs.compositor] service = true serves = ["compositor", "prompt", "clipboard", "input"] receives = ["soundserver", "filepicker", "volume"] -devices = ["framebuffer", "keyboard", "mouse"] +devices = ["pci:1af4:1050", "framebuffer", "keyboard", "mouse"] starts = ["terminal", "files", "/apps"] login = true diff --git a/issues/a-class-claims-call-in-flight-acts-after-the-claims-release.md b/issues/a-class-claims-call-in-flight-acts-after-the-claims-release.md index 00e946a2c00..72ba0a42ef5 100644 --- a/issues/a-class-claims-call-in-flight-acts-after-the-claims-release.md +++ b/issues/a-class-claims-call-in-flight-acts-after-the-claims-release.md @@ -6,29 +6,22 @@ opened: 2026-10-08 # A class claim's call in flight acts after the claim's release -A claim on a class this kernel still drives — the framebuffer, HDA audio — is -a per-class flag (`device::TAKEN`), and a call on one checks -the handle and then acts with nothing held across the two: - -- `SYS_DEVICE_REG_READ` and `SYS_DEVICE_REG_WRITE` resolve the claim to - `RegTarget::Hda` under the process-data lock (`sys_device_reg`, - `kernel/src/syscall/device.rs`) and reach `drivers::hda::reg_write` after - it; -- `SYS_GPU_PRESENT`, `SYS_GPU_SET_CURSOR`, `SYS_GPU_MOVE_CURSOR` and - `SYS_GPU_SET_RESOLUTION` ask `holds_claim` and then call `crate::gpu` - (`kernel/src/syscall/dispatch.rs`). +A claim on a class this kernel still drives — HDA audio — is a per-class flag +(`device::TAKEN`), and a call on one checks the handle and then acts with +nothing held across the two: `SYS_DEVICE_REG_READ` and `SYS_DEVICE_REG_WRITE` +resolve the claim to `RegTarget::Hda` under the process-data lock +(`sys_device_reg`, `kernel/src/syscall/device.rs`) and reach +`drivers::hda::reg_write` after it. The flag clears when the claim's last handle goes (`Claim`'s drop, `kernel/src/device.rs`) and the next claim sets it again. So a call whose claim a sibling thread closed after the check, and whose class another process -claimed before the act, writes a register of the audio controller, or presents -on the display, that the second process now holds. +claimed before the act, writes a register of the audio controller that the +second process now holds. A claim on a PCI function, an ISA function or the ACPI fixed hardware does not have this: its calls borrow what names the device from the claim under the -lock its release takes it with (`DeviceClaim::pci`, `DeviceClaim::isa`). That -borrow is no fix to copy here as it stands: `SYS_GPU_SET_RESOLUTION` allocates, -and the claim's lock is a spinlock. +lock its release takes it with (`DeviceClaim::pci`, `DeviceClaim::isa`). **A poll has the same shape on these classes.** The watch a poll on an audio claim or the mouse's registers on is the class's one `static` diff --git a/issues/a-domains-addresses-are-never-given-back.md b/issues/a-domains-addresses-are-never-given-back.md index 009a8132a15..9bd28254617 100644 --- a/issues/a-domains-addresses-are-never-given-back.md +++ b/issues/a-domains-addresses-are-never-given-back.md @@ -9,11 +9,13 @@ opened: 2026-09-03 `Domain::reserve` (`kernel/src/arch/x86_64/vtd/table.rs:213-224`) is a monotonic bump: an address is never handed out twice, unmapped or not, and `unmap` returns no range. A driver that maps and unmaps repeatedly therefore consumes -its domain's device address space for good. The display is the first such -driver — every `set_resolution` maps a new scanout and unmaps the old one, and -one boot with three mode changes walked `device=0x400000200000`, -`0x400000800000`, `0x400000a00000`, `0x400000e00000` with nothing between them -reusable. From `first_address = 1 << (bits - 2)` and the `1 << bits` ceiling, a +its domain's device address space for good. The kernel's virtio-gpu driver +was the first such driver — every mode change mapped a new scanout and +unmapped the old one, and one boot with three mode changes walked +`device=0x400000200000`, `0x400000800000`, `0x400000a00000`, +`0x400000e00000` with nothing between them reusable; the compositor lends its +scanouts now, in a window whose addresses are lent again +(`kernel/src/pcidev/mod.rs`). From `first_address = 1 << (bits - 2)` and the `1 << bits` ceiling, a 39-bit unit (the harness's `narrow` shape) has 196,608 leaves to give before a mode change is refused with `ResourceExhausted`; the refusal is the right answer to exhaustion and the wrong thing to reach by changing modes. diff --git a/issues/a-lent-scanout-is-one-contiguous-run.md b/issues/a-lent-scanout-is-one-contiguous-run.md new file mode 100644 index 00000000000..dba0a71f76a --- /dev/null +++ b/issues/a-lent-scanout-is-one-contiguous-run.md @@ -0,0 +1,21 @@ +--- +status: open +kind: defect +opened: 2026-10-14 +--- + +# A lent scanout is one physically contiguous run + +The compositor lends each virtio-gpu scanout as one region +(`system/compositor/src/display.rs`, `Claim::lend`), and a region the kernel +makes is one physically contiguous run of 2 MiB pages +(`kernel/src/object/shm.rs`, `SharedMemObject::create`). A mode at the +largest bound is 64 such pages; on a machine whose memory is fragmented there +may be no run that long, and the mode change is then declined by name and the +old mode stays, though the memory exists. Virtio 1.2 §5.7.6.1 already takes a +scatter list ("the framebuffer doesn't need to be contignous in guest +physical memory"), and `RESOURCE_ATTACH_BACKING` takes one entry per run. + +Owner: the memory track. Exit: a region backed by more than one run, lent as +one mapping in the function's domain or as one entry per run, and a mode +change on a machine with no run of its length served. diff --git a/issues/a-refused-framebuffer-reads-as-no-display.md b/issues/a-refused-framebuffer-reads-as-no-display.md index dcd7322f657..6e48e2b8eca 100644 --- a/issues/a-refused-framebuffer-reads-as-no-display.md +++ b/issues/a-refused-framebuffer-reads-as-no-display.md @@ -31,11 +31,11 @@ metal-sim boot with `mute: true` and the T14 itself do not have. Nothing reaches the kernel, so nothing reaches `/log`, the panel, or a crash report. `main.rs:520` then branches on `kernel_args.gop_framebuffer != 0` and takes the -same arm for both. On this harness the second shape is rescued by -`virtio_gpu::init` claiming the same device and logging `GPU: using VirtIO`. On a +same arm for both. On this harness the second shape is rescued by the +compositor's `pci:1af4:1050` claim on the same device. On a machine whose Blt-only display has no kernel driver — which the T14 is until an Intel display engine driver exists — the branch falls to `main.rs:529` and both -shapes print `GPU: none found, running headless`. That inference is from the +shapes print `GPU: the firmware handed over no framebuffer`. That inference is from the branch, not measured: QEMU cannot stage a Blt-only GOP on a device this kernel has no driver for. diff --git a/issues/a-restarted-display-holder-lends-around-its-predecessors-scanouts.md b/issues/a-restarted-display-holder-lends-around-its-predecessors-scanouts.md new file mode 100644 index 00000000000..4c805a19bfa --- /dev/null +++ b/issues/a-restarted-display-holder-lends-around-its-predecessors-scanouts.md @@ -0,0 +1,26 @@ +--- +status: open +kind: defect +opened: 2026-10-14 +--- + +# A restarted display holder lends around its predecessor's scanouts + +A virtio function on q35's root bus has no function-level reset and no power +management capability, so `pcidev`'s release cannot reset it: it keeps every +address the last holder's grants and lends were at as the function's residue +(`kernel/src/pcidev/mod.rs`, `Function::residue`), and a residue address is +never lent again (`Bound::lend_room`). A compositor started again on the same +virtio-gpu therefore lends its scanouts in `toyos_abi::pci::LEND_WINDOW` less +what the last one lent: after a holder at the largest mode, which lends two +scanouts' worth, the window has no room for a flip at that mode and it is +declined by name. QEMU keeps the old holder's resources, `max_hostmem` +included, until the next holder's own reset (§3.1.1, which +`toyos-virtio`'s `Offer::acknowledge` makes first). + +The supervisor does not restart the compositor, so nothing reaches this +today. + +Exit: a residue a holder's own §3.1.1 reset retires — the function's status +read 0 after the reset is the proof it holds nothing — so the window is whole +again for the next holder. diff --git a/issues/every-driver-is-still-in-the-kernel.md b/issues/every-driver-is-still-in-the-kernel.md index 9c5cfc35ee5..cae03b9e345 100644 --- a/issues/every-driver-is-still-in-the-kernel.md +++ b/issues/every-driver-is-still-in-the-kernel.md @@ -9,9 +9,11 @@ opened: 2026-08-02 The owner's target is that `virtio` does not appear anywhere in the kernel. The strongest check on it is not the driver files but the ABI: no syscall name may carry a NIC, GPU or audio device operation. `rg '^pub const SYS_(NIC|GPU|AUDIO)_' -sdk/abi/src/syscall.rs` is the check — the three `SYS_NIC_*` names went with -the virtio NIC driver, which is `userland/netd/src/virtio_net.rs` now, and the -`SYS_GPU_*` names are what is left. +sdk/abi/src/syscall.rs` is the check, and it finds none: the three `SYS_NIC_*` +names went with the virtio NIC driver, which is `system/netstack/src/virtio_net.rs` +now, and the four `SYS_GPU_*` names with virtio-gpu's, which is the +compositor's (`system/compositor/virtio-gpu`). The kernel's virtio code left is +its console's. **Ordering ruling, and it is not negotiable: the IOMMU lands first and completely, interrupt remapping included, and no driver leaves the kernel before @@ -29,32 +31,31 @@ is not built. What is left of the staged work: -1. **HDA and virtio-gpu, re-scoped.** `drivers/hda.rs` brings its device up - and gates soundserver's register access; `drivers/virtio_gpu.rs` is the only - `Gpu` whose `SYS_GPU_*` calls do anything, since GOP's are all no-ops. Each - leaves when its userland holder claims the function as `pci`, as netstack - and soundserver's virtio-sound driver do, retiring the `hda-audio` class, - its arms of `SYS_DEVICE_REG_READ`/`WRITE`, and `SYS_GPU_*`, which is an ABI - change. GOP stays: it is memory the loader hands over, and the panic console - paints it. HDA leaving feeds soundserver's DLL from a claim's record on - every machine, so that diff lands +1. **HDA, re-scoped.** `drivers/hda.rs` brings its device up and gates + soundserver's register access. It leaves when soundserver claims the + function as `pci`, as it does its virtio-sound function, retiring the + `hda-audio` class and its arms of `SYS_DEVICE_REG_READ`/`WRITE`. HDA + leaving feeds soundserver's DLL from a claim's record on every machine, so + that diff lands `issues/soundservers-virtio-clock-can-take-a-period-one-notification-early.md`'s - exit. + exit. GOP stays: it is memory the loader hands over, and the panic console + paints it. A virtio holder stands on `toyos-virtio`, which walks the capability list too. Before a client ends its device on `UsedRefusal::Written` for a chain the device only reads, whose bound is 0, what QEMU's device reports as `len` on such a queue is measured: the NIC's transmit queue is the only one read so far. - `iommu_virtio_platform`'s decline control, `declining_is_not_free` - (`tests/common/iommu.rs`), declines through the kernel's - `virtio-no-access-platform` actuator, which reaches only a virtio function - the kernel drives; with the NIC and virtio-sound in processes that is the - virtio-gpu `Profile::HeadlessVirtioGpu` adds for it, and virtio-gpu leaving - leaves the control nothing to decline. Exit for the control, in the diff - that moves virtio-gpu out: the decline is made where the features are - negotiated, `toyos-virtio`'s `Offer::accept`, for a function a process - drives, and the control reds when that function keeps `FEATURES_OK`; - `Profile::HeadlessVirtioGpu` goes in the same diff. + Done for virtio-gpu: the compositor drives it through a `pci:1af4:1050` + claim. Its cursor queue's `len` is measured: QEMU writes 0 for a chain + with a writable answer and for one without (PR #895's evidence, `pr-895` + at `7d47ad2b`). The decline control `declining_is_not_free` and the + `virtio-no-access-platform` actuator went with it, by the orchestrator's + ruling on PR #895, rather than the decline moving into `Offer::accept`: + with the kernel driving only its console the actuator reached nothing, + and what ToyOS relies on is `toyos-virtio`'s, host-tested — the bit + accepted wherever offered, and a device that drops `FEATURES_OK` + refused. `Profile::HeadlessVirtioGpu` stays, the suite's one machine whose + display a process drives. 2. Done: **BAR sizing and re-assignment onto 2 MiB boundaries** is `pcidev::place_bar`, with the overlap refusal kept as the assertion that it worked rather than as the mechanism. diff --git a/issues/the-desktop-draws-at-the-panels-scale.md b/issues/the-desktop-draws-at-the-panels-scale.md index fbdc51ecace..71972ad42e8 100644 --- a/issues/the-desktop-draws-at-the-panels-scale.md +++ b/issues/the-desktop-draws-at-the-panels-scale.md @@ -27,11 +27,14 @@ What is left: drawing its chrome, text, icons and cursor at it and relaying windows out on a change, terminal, filepicker and `files` at the scale, and the winit fork's `scale_factor()`. -2. **virtio-gpu leaves the kernel** (stage 1 of +2. Done: **virtio-gpu left the kernel** (stage 1 of `issues/every-driver-is-still-in-the-kernel.md`): the compositor drives it - through a `pci:1af4:1050` claim and follows the host window's display - events. Blocked on `issues/a-cargo-run-boot-claims-every-pcidev-slot.md`: - the Virtio profile's fifth claim is refused until a slot is free. + through a `pci:1af4:1050` claim, takes the mode the device prefers and + flips to the one each display event asks for, and + `the_compositor_drives_the_virtio_gpu` resizes QEMU's display over VNC and + reads the guest's new mode. The scale is chosen per event and drawn at by + nothing yet, as on GOP; `screen WxH` is still the window protocol's + `MSG_SET_RESOLUTION` until the `display` port of item 1. Constraints already paid for: diff --git a/issues/the-gpu-driver-asserts-on-what-the-device-answers.md b/issues/the-gpu-driver-asserts-on-what-the-device-answers.md deleted file mode 100644 index a3642f004ec..00000000000 --- a/issues/the-gpu-driver-asserts-on-what-the-device-answers.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-03 ---- - -# The GPU driver asserts on what the device answers - -Six `assert!`s in `kernel/src/drivers/virtio_gpu.rs` panic the kernel on a -device answer other than `RESP_OK_NODATA`, on paths userland drives: lines 294 -(`RESOURCE_CREATE_2D`), 304 (`RESOURCE_UNREF`), 309 (`RESOURCE_ATTACH_BACKING`), -350 (`SET_SCANOUT`), 362 (`TRANSFER_TO_HOST_2D`) and 373 (`RESOURCE_FLUSH`) — -the first four on `set_resolution`, the last two on every `present_rect`. A -device's answer crossed a trust boundary and is refused, never asserted; the -resource-id collision the second mode change used to hit was one instance of -this class (the answer was `0x1203`, and the kernel died on it), and the fix -removed the instance, not the class. - -Exit: each answer becomes a refusal to the caller by name; a device answer that -cannot be refused is a kernel bug and says so. diff --git a/issues/the-second-scanout-buffer-is-read-by-nobody.md b/issues/the-second-scanout-buffer-is-read-by-nobody.md deleted file mode 100644 index 9dc7556de02..00000000000 --- a/issues/the-second-scanout-buffer-is-read-by-nobody.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-03 ---- - -# The second scanout buffer is read by nobody - -`alloc_framebuffer` in `kernel/src/drivers/virtio_gpu.rs` allocates two whole -framebuffers of contiguous 2 MiB pages per mode and mints both as -`FramebufferInfo::scanout`; the device is given `scanout[0]` only, and so is -every reader — `system/compositor/src/session.rs` (lines 154 and 949) and -`system/console/src/main.rs` (line 99) adopt `scanout[0]` and nothing adopts -`scanout[1]`. Exit: the second buffer is deleted, or something reads it. diff --git a/issues/toyos-runs-on-arm64.md b/issues/toyos-runs-on-arm64.md index 351ab118fee..06d52a371c1 100644 --- a/issues/toyos-runs-on-arm64.md +++ b/issues/toyos-runs-on-arm64.md @@ -327,8 +327,8 @@ Each stage names its exit; "measured" means a number from a run. stage's SMMUv3 first. Stubbed on AArch64, each owned by the small-kernel track, which moves the driver out of the kernel: - a driver in this kernel is given no message (`irqchip::its::msi`), so - the kernel's xHCI (`virt`'s boot stick), HDA, virtio-console and - virtio-gpu drivers each refuse their function by name. + the kernel's xHCI (`virt`'s boot stick), HDA and virtio-console + drivers each refuse their function by name. - `drivers::gop` refuses a scanout that is not whole 2 MiB pages of its own, which a `ramfb` scanout carved out of RAM need not be. diff --git a/issues/volatile-composites-on-mmio-dma-structs.md b/issues/volatile-composites-on-mmio-dma-structs.md index 53a5b65f525..4e7cecf15ed 100644 --- a/issues/volatile-composites-on-mmio-dma-structs.md +++ b/issues/volatile-composites-on-mmio-dma-structs.md @@ -9,12 +9,12 @@ 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, which left the kernel with it; the rest: +the NVMe driver's queue entries and one virtio-gpu's EDID answer, which left +the kernel with their drivers; the rest: - `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. -- `drivers/virtio_gpu.rs:276` — `RespEdid`. The lint's own docs put the reason plainly: volatile ops are well-defined for a primitive because the compiler must emit exactly the load/store the source diff --git a/issues/whether-a-region-may-be-lent-is-a-runtime-option.md b/issues/whether-a-region-may-be-lent-is-a-runtime-option.md index a0e3bc40a8e..19b7f408efe 100644 --- a/issues/whether-a-region-may-be-lent-is-a-runtime-option.md +++ b/issues/whether-a-region-may-be-lent-is-a-runtime-option.md @@ -16,9 +16,7 @@ virtio-sound's pool, which left the kernel with its driver; the test is out of the guest suite and staged as a T14 row in `issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md`'s stage J, where the kernel driver's pool it can refuse is HDA's. And the field's name -says nothing about lending: the virtio-gpu scanout and cursor carry `Some` and -are lendable today, which is the same authority their holder already has but -was decided by nobody. +says nothing about lending. **Exit condition.** A region's kind is a type — owned pages, a kernel driver's pool, an aperture — and `dma_map` takes only the owned-pages kind, so a region diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index e0dd3f8b65e..df27655f356 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -120,9 +120,6 @@ actuators! { /// a quantum, and take its interrupts with them open. timer_floor = "timer-floor"; - /// Withhold `VIRTIO_F_ACCESS_PLATFORM` from every virtio device but the console, staging a function whose addresses the unit never translates. - virtio_no_access_platform = "virtio-no-access-platform"; - /// Leave every IOMMU unit as firmware may hand one over, just before this /// kernel takes it: a VT-d unit queueing, translating and remapping /// through tables of its own, an AMD-Vi unit blocking every device's DMA diff --git a/kernel/src/device.rs b/kernel/src/device.rs index 66c71e75596..536f30d6138 100644 --- a/kernel/src/device.rs +++ b/kernel/src/device.rs @@ -1,6 +1,6 @@ use alloc::sync::Arc; -use crate::object::device::{DeviceClaim, DeviceInfo, FramebufferBuffers}; +use crate::object::device::{DeviceClaim, DeviceInfo}; use crate::object::shm::Region; use crate::{keyboard, mouse}; use toyos_abi::FramebufferInfo; @@ -12,16 +12,15 @@ static TAKEN: [Lock; DeviceType::ALL.len()] = [const { Lock::new(false) }; DeviceType::ALL.len()]; static FB_INFO: Lock> = Lock::new(None); -/// What the display driver published, as a claim needs it. +/// The firmware's framebuffer, as a claim needs it. /// -/// Screen stores `Region`s rather than `SharedMemObject`s because each claim -/// mints its own object, and a `SharedMemObject` is retired once its handle -/// count reaches zero while the screen itself outlives any single claim. +/// A `Region` rather than a `SharedMemObject` because each claim mints its own +/// object, and a `SharedMemObject` is retired once its handle count reaches +/// zero while the screen outlives any single claim. #[derive(Clone)] pub struct Screen { pub info: FramebufferInfo, - pub scanout: [Region; 2], - pub cursor: Region, + pub scanout: Region, } fn taken(class: DeviceType) -> &'static Lock { @@ -288,11 +287,8 @@ fn shm(region: Region) -> Arc { crate::object::shm::SharedMemObject::over(region) } -/// The description a framebuffer claim answers with, over freshly minted buffer objects. -pub fn framebuffer_info(screen: Screen) -> DeviceInfo { - let Screen { info, scanout: [front, back], cursor } = screen; - DeviceInfo::Framebuffer( - info, - FramebufferBuffers { scanout: [shm(front), shm(back)], cursor: shm(cursor) }, - ) +/// The description a framebuffer claim answers with, over a freshly minted +/// scanout object. +fn framebuffer_info(screen: Screen) -> DeviceInfo { + DeviceInfo::Framebuffer(screen.info, shm(screen.scanout)) } diff --git a/kernel/src/drivers/gop.rs b/kernel/src/drivers/gop.rs index c01ce97ec31..a733cff43cd 100644 --- a/kernel/src/drivers/gop.rs +++ b/kernel/src/drivers/gop.rs @@ -1,29 +1,12 @@ -use alloc::boxed::Box; - -use toyos_abi::syscall::SyscallError; +//! The firmware's framebuffer, as the loader handed it over: what a +//! `framebuffer` claim maps. +use crate::device::Screen; use crate::mm::policy::{CachePolicy, MmioPolicy}; -use crate::mm::{PAGE_2M, align_2m_checked, DirectMap}; -use crate::gpu::{Gpu, GpuInfo}; +use crate::mm::{align_2m_checked, DirectMap}; use crate::log; use crate::object::shm::Region; -struct GopGpu; - -impl Gpu for GopGpu { - fn present_rect(&mut self, _x: u32, _y: u32, _w: u32, _h: u32) { - // GOP framebuffer is memory-mapped — writes are immediately visible. - } - - fn set_cursor(&mut self, _hot_x: u32, _hot_y: u32) {} - fn move_cursor(&mut self, _x: u32, _y: u32) {} - - fn set_resolution(&mut self, _width: u32, _height: u32) -> Result { - // GOP cannot change resolution after UEFI boot services exit. - Err(SyscallError::NotSupported) - } -} - /// `addr` is the physical address of the framebuffer supplied by firmware. /// `None` is a scanout no process can be given: one is handed over in 2 MiB /// pages, and the architecture says whether this one's are its own. @@ -35,7 +18,7 @@ pub fn init( stride: u32, pixel_format: u32, edid: ([u8; toyos_abi::boot::EDID_BLOCK], u32), -) -> Option<(Box, GpuInfo)> { +) -> Option { if let Err(why) = crate::mm::paging::scanout_whole_pages(addr, size) { log!("GOP: the scanout at {addr:#x}+{size:#x} is refused: {why}"); return None; @@ -55,14 +38,12 @@ pub fn init( // must match the cache policy used for the client mapping below. crate::mm::paging::map_mmio(addr, aligned_size, MmioPolicy::WriteCombining); - let fb = DirectMap::from_phys(addr); - // GOP has no second buffer; front and back scanout regions alias the same memory. - let scanout = core::array::from_fn(|_| Region { - phys: fb, + let scanout = Region { + phys: DirectMap::from_phys(addr), size: aligned_size, cache: CachePolicy::WriteCombining, pages: None, - }); + }; log!("GOP: {}x{} stride={} format={} at {:#x}", width, height, stride, pixel_format, addr); @@ -78,29 +59,18 @@ pub fn init( ); log!("GOP: scanout memory type {memory_type}"); - let cursor_pages = crate::mm::pmm::alloc_contiguous(1).expect("GOP: cursor alloc failed"); - let cursor_phys = cursor_pages[0].direct_map().phys(); - // Cursor buffer is plain system RAM, not scanout, so it keeps the default write-back type. - let cursor = Region { - phys: DirectMap::from_phys(cursor_phys), - size: PAGE_2M, - cache: CachePolicy::Normal, - pages: None, - }; - #[expect(clippy::disallowed_methods, reason = "the GPU is never torn down, so its cursor pages outlive every process")] - core::mem::forget(cursor_pages); - - let info = GpuInfo { + Some(Screen { + info: toyos_abi::FramebufferInfo { + // Minted per claim by `try_claim`: the process reading it does not + // exist yet. + scanout: toyos_abi::HANDLE_INVALID, + width, + height, + stride, + pixel_format, + edid: edid.0, + edid_len: edid.1, + }, scanout, - cursor, - width, - height, - stride, - pixel_format, - flags: 0, - edid: edid.0, - edid_len: edid.1, - }; - - Some((Box::new(GopGpu), info)) + }) } diff --git a/kernel/src/drivers/mod.rs b/kernel/src/drivers/mod.rs index d089972afcf..0da61501be4 100644 --- a/kernel/src/drivers/mod.rs +++ b/kernel/src/drivers/mod.rs @@ -12,7 +12,6 @@ pub mod xhci; pub mod usb_storage; pub mod virtio; pub mod virtio_console; -pub mod virtio_gpu; pub mod gop; pub mod hda; pub mod panic_console; diff --git a/kernel/src/drivers/panic_console/mod.rs b/kernel/src/drivers/panic_console/mod.rs index fbef70eebce..da7cac24beb 100644 --- a/kernel/src/drivers/panic_console/mod.rs +++ b/kernel/src/drivers/panic_console/mod.rs @@ -3,8 +3,8 @@ //! Renders log records as an 8x16 text grid onto the UEFI GOP framebuffer, //! through `LogRecord`'s `Display`, so no second formatter can drift from //! `logkeeper`. [`capture`] freezes the report before `panic_flush` drains it; -//! [`render`] paints it inside `halt_all_cpus`, before `panic_flush`. virtio-gpu -//! is unsupported: its scanout needs the unbounded-poll wedge this module avoids. +//! [`render`] paints it inside `halt_all_cpus`, before `panic_flush`. A display +//! a process drives, virtio-gpu's, is no scanout this module can reach. //! //! The hold this module ends a panic in, [`hold_the_panel`], is also where //! `crate::panic_reboot`'s bound is watched. It reads no input and scrolls @@ -341,36 +341,28 @@ static PENDING: FbCell = FbCell(UnsafeCell::new(Fb::DETACHED)); static RAW_PHYS: AtomicU64 = AtomicU64::new(0); static RAW_SIZE: AtomicU64 = AtomicU64::new(0); -/// Boot-time and `set_resolution`-window only: publishers never race. +/// Boot-time only: publishers never race. fn publish(fb: Fb) { forget_the_glass(); FB.publish(fb.words()); } -/// Stop painting until the next [`rearm`], for a window where the framebuffer may be freed and reallocated. -/// Worst case becomes no console, never a write into memory that has been freed and reused. +/// Stop painting until the next [`rearm`]: no console, never a write into +/// memory the scanout no longer is. pub fn detach() { publish(Fb::DETACHED); } -/// Re-publish the armed descriptor after such a window. +/// Re-publish the armed descriptor. pub fn rearm() { - // SAFETY: `PENDING` has one writer at a time (`arm`/`disable`, - // single-threaded, or `set_resolution`'s single-CPU window); `Fb` is `Copy`, so this holds no reference into the cell. + // SAFETY: `PENDING`'s one writer is `arm`, single-threaded at boot; `Fb` + // is `Copy`, so this holds no reference into the cell. let fb = unsafe { *PENDING.0.get() }; if validate(&fb) { publish(fb); } } -/// Give up the display permanently when virtio-gpu wins device selection; clearing `PENDING` stops a later `rearm` resurrecting it. -pub fn disable() { - // SAFETY: sound as `rearm`'s read — `PENDING` has one writer, called from the single-threaded boot sequence. - unsafe { *PENDING.0.get() = Fb::DETACHED }; - RAW_PHYS.store(0, Ordering::Relaxed); - detach(); -} - /// One publication whole, or nothing: a descriptor still changing is no /// screen this instant. fn snapshot() -> Option { @@ -453,7 +445,7 @@ pub fn arm(args: &KernelArgs, maps: &[MemoryMapEntry]) { return; } - // SAFETY: sound as `disable`'s write — `arm` runs once, single-threaded, before any AP starts. + // SAFETY: `arm` runs once, single-threaded, before any AP starts. unsafe { *PENDING.0.get() = fb }; RAW_PHYS.store(args.gop_framebuffer, Ordering::Relaxed); RAW_SIZE.store(args.gop_framebuffer_size, Ordering::Relaxed); @@ -499,7 +491,7 @@ pub fn arm(args: &KernelArgs, maps: &[MemoryMapEntry]) { uefi_type ); // Cleared too, so a later `rearm` cannot resurrect a scanout the PMM owns. - // SAFETY: sound as `disable`'s write — one writer, in the single-threaded boot sequence. + // SAFETY: still `arm`'s single-threaded run, before any AP starts. unsafe { *PENDING.0.get() = Fb::DETACHED }; RAW_PHYS.store(0, Ordering::Relaxed); detach(); diff --git a/kernel/src/drivers/virtio.rs b/kernel/src/drivers/virtio.rs index 9133258bc5a..7b7fa3def50 100644 --- a/kernel/src/drivers/virtio.rs +++ b/kernel/src/drivers/virtio.rs @@ -159,18 +159,6 @@ const RESET: crate::time::Budget = crate::time::Budget::of( "the device is refused, never waited on", ); -/// Every driver's accepted set carries [`VIRTIO_F_ACCESS_PLATFORM`]; the actuator -/// withholds it to stage a function no unit sees, sparing the console. -fn platform_addressing(pci_dev: &PciDevice) -> u64 { - #[cfg(feature = "boot-actuators")] - if crate::actuator::virtio_no_access_platform() && pci_dev.device_id() != 0x1043 { - return 0; - } - #[cfg(not(feature = "boot-actuators"))] - let _ = pci_dev; - VIRTIO_F_ACCESS_PLATFORM -} - /// The config sub-window a virtio PCI capability names, or why it names none: /// the device's index, offset and length checked before a subregion is taken. fn cap_subwindow( @@ -693,9 +681,9 @@ pub fn cap_selftest() { } // A refused device keeps no bus mastering: COMMAND arrives armed (bit 2, Bus - // Master Enable, PCI 3.0 §6.2.2) and `init`'s refusal must clear it. + // Master Enable, PCI 3.0 §6.2.2) and `offer`'s refusal must clear it. cfg.write_u16(0x04, 0x0006); - let refused = VirtioDevice::init(&device, 0); + let refused = VirtioDevice::offer(&device); let command = cfg.read_u16(0x04); match refused { Err(InitRefusal::Cap(MissingCap::Common)) if command & 0x4 == 0 => passed += 1, @@ -739,7 +727,7 @@ impl Offered { pci_dev.enable_bus_master(); let common = config.common; - let features = device_features & (accepted_features | platform_addressing(pci_dev)); + let features = device_features & (accepted_features | VIRTIO_F_ACCESS_PLATFORM); log!( "VirtIO: PCI {:02x}:{:02x}.{} features device={device_features:#x} \ negotiated={features:#x} access_platform={}", @@ -804,11 +792,6 @@ impl VirtioDevice { Ok(Offered { config, features }) } - /// Initialize a VirtIO PCI device: reset, negotiate features, prepare for queue setup. - pub fn init(pci_dev: &PciDevice, accepted_features: u64) -> Result { - Self::offer(pci_dev)?.accept(pci_dev, accepted_features) - } - /// Configure a virtqueue's addresses and size; does not enable it — call `enable_queue()` after MSI-X vectors are set. pub fn setup_queue(&self, index: u16, queue: &mut Virtqueue<'_>) { let common = self.config.common; diff --git a/kernel/src/drivers/virtio_gpu.rs b/kernel/src/drivers/virtio_gpu.rs deleted file mode 100644 index 52fe40b0ddb..00000000000 --- a/kernel/src/drivers/virtio_gpu.rs +++ /dev/null @@ -1,649 +0,0 @@ -use alloc::boxed::Box; - -use super::pci::PciDevice; -use super::virtio::{BufDir, DescSlot, Virtqueue, VirtioDevice, VIRTIO_F_VERSION_1}; -use super::DmaPool; -use toyos_abi::syscall::SyscallError; -use crate::iommu::{DeviceSpace, IommuError}; -use crate::mm::{Dma, Unaligned, PAGE_2M}; -use crate::gpu::{FLAG_HARDWARE_CURSOR, Gpu, GpuInfo}; -use crate::log; -use crate::mm::policy::CachePolicy; -use crate::object::shm::{Pages, Region}; - -const VIRTIO_VENDOR: u16 = 0x1AF4; -const VIRTIO_GPU_DEVICE: u16 = 0x1050; // 0x1040 + device_id 16 - -const CMD_RESOURCE_CREATE_2D: u32 = 0x0101; -const CMD_RESOURCE_UNREF: u32 = 0x0102; -const CMD_SET_SCANOUT: u32 = 0x0103; -const CMD_RESOURCE_FLUSH: u32 = 0x0104; -const CMD_TRANSFER_TO_HOST_2D: u32 = 0x0105; -const CMD_RESOURCE_ATTACH_BACKING: u32 = 0x0106; -const CMD_GET_EDID: u32 = 0x010a; -const CMD_UPDATE_CURSOR: u32 = 0x0300; -const CMD_MOVE_CURSOR: u32 = 0x0301; - -const RESP_OK_NODATA: u32 = 0x1100; -const RESP_OK_EDID: u32 = 0x1104; - -const VIRTIO_GPU_F_EDID: u64 = 1 << 1; - -const FORMAT_B8G8R8A8_UNORM: u32 = 1; -const FORMAT_B8G8R8X8_UNORM: u32 = 2; - -const OFF_CONTROLQ: usize = 0x0000; -const OFF_CONTROLQ_BUFS: usize = 0x1000; -const OFF_CURSORQ: usize = 0x2000; -const OFF_CURSORQ_BUFS: usize = 0x3000; -const DMA_SIZE: usize = 0x4000; - -const CURSOR_SIZE: u32 = 64; -const CURSOR_RESOURCE_ID: u32 = 1; -/// Scanouts count up from here, so no mode change ever reuses the cursor's id. -const FIRST_SCANOUT_RESOURCE_ID: u32 = 2; - -const REQ_OFFSET: usize = 0x000; -const RESP_OFFSET: usize = 0x800; - -#[repr(C)] -#[derive(Clone, Copy)] -struct CtrlHeader { - cmd_type: u32, - flags: u32, - fence_id: u64, - ctx_id: u32, - ring_idx: u8, - padding: [u8; 3], -} - -impl CtrlHeader { - fn new(cmd_type: u32) -> Self { - Self { cmd_type, flags: 0, fence_id: 0, ctx_id: 0, ring_idx: 0, padding: [0; 3] } - } -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct Rect { - x: u32, - y: u32, - width: u32, - height: u32, -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct ResourceCreate2d { - hdr: CtrlHeader, - resource_id: u32, - format: u32, - width: u32, - height: u32, -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct ResourceUnref { - hdr: CtrlHeader, - resource_id: u32, - padding: u32, -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct SetScanout { - hdr: CtrlHeader, - r: Rect, - scanout_id: u32, - resource_id: u32, -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct ResourceFlush { - hdr: CtrlHeader, - r: Rect, - resource_id: u32, - padding: u32, -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct TransferToHost2d { - hdr: CtrlHeader, - r: Rect, - offset: u64, - resource_id: u32, - padding: u32, -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct ResourceAttachBacking { - hdr: CtrlHeader, - resource_id: u32, - nr_entries: u32, -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct MemEntry { - addr: u64, - length: u32, - padding: u32, -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct CursorPos { - scanout_id: u32, - x: u32, - y: u32, - padding: u32, -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct UpdateCursor { - hdr: CtrlHeader, - pos: CursorPos, - resource_id: u32, - hot_x: u32, - hot_y: u32, - padding: u32, -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct GetEdid { - hdr: CtrlHeader, - scanout: u32, - padding: u32, -} - -#[repr(C)] -#[derive(Clone, Copy)] -struct RespEdid { - hdr: CtrlHeader, - size: u32, - padding: u32, - edid: [u8; 1024], -} - -/// Pages the device can reach, for as long as it may reach them: the device -/// address is the attachment's and not the pages', which a holder's -/// `Arc` keeps alive past every scanout they backed. -struct AttachedBacking { - space: DeviceSpace, - at: u64, - bytes: u64, -} - -impl AttachedBacking { - fn take(space: DeviceSpace, phys: u64, bytes: u64) -> Result { - let at = space.map(phys, bytes)?; - Ok(Self { space, at, bytes }) - } -} - -impl Drop for AttachedBacking { - fn drop(&mut self) { - self.space - .unmap(self.at, self.bytes) - .unwrap_or_else(|why| panic!("VirtIO GPU: {why}")); - } -} - -/// Ref-counted scanout pages: a resolution change drops this without forcing -/// existing holders (e.g. a compositor) to unmap. -struct FbAlloc { - regions: [Region; 2], - /// `regions[0]`'s; `None` only for the placeholder built before the display. - backing: Option, -} - -impl FbAlloc { - fn device_addr(&self) -> u64 { - self.backing.as_ref().expect("VirtIO GPU: a framebuffer the device was never given").at - } -} - -struct GpuController { - device: VirtioDevice, - space: DeviceSpace, - controlq: Virtqueue<'static>, - cursorq: Virtqueue<'static>, - control_slot: Option, - cursor_slot: Option, - /// req buffers are `Unaligned` (CPU-written); resp buffers are volatile - /// (device-written). - req: Dma<'static, Unaligned>, - resp: Dma<'static>, - cursor_req: Dma<'static, Unaligned>, - #[allow(dead_code)] // the cursor queue's answers are not read - cursor_resp: Dma<'static>, - width: u32, - height: u32, - resource: u32, - fb: FbAlloc, - cursor: Region, - cursor_backing: Option, -} - -impl GpuController { - /// Reads what the device wrote into the response buffer. - fn answer(&self) -> T { - // Safe only because `submit_and_wait`'s `barrier::dma_rmb` already - // ordered the device's write before this read. - self.resp.read(0) - } - - /// Writes `req` into the request buffer, submits it, and returns what the - /// device answered. - fn command_of(&mut self, req: &Req) -> Resp { - self.req.write(0, *req); - - let slot = self.control_slot.take().expect("GPU: no control slot"); - let returned = self.controlq.submit_and_wait( - slot, - &[ - (self.req.device_addr(), core::mem::size_of::() as u32, BufDir::Readable), - (self.resp.device_addr(), core::mem::size_of::() as u32, BufDir::Writable), - ], - self.device.notify_mmio(), - self.device.notify_off_multiplier(), - 0, // controlq index - ); - self.control_slot = Some(returned); - - self.answer() - } - - /// Every command but GET_EDID: answers with just the response header's - /// type field. - fn command(&mut self, req: &T) -> u32 { - // CtrlHeader sized, not u32: the device must be told how much room it - // has to write. - let hdr: CtrlHeader = self.command_of(req); - hdr.cmd_type - } - - fn get_edid(&mut self, scanout: u32) -> RespEdid { - let cmd = GetEdid { - hdr: CtrlHeader::new(CMD_GET_EDID), - scanout, - padding: 0, - }; - self.command_of(&cmd) - } - - fn create_resource(&mut self, id: u32, format: u32, width: u32, height: u32) { - let cmd = ResourceCreate2d { - hdr: CtrlHeader::new(CMD_RESOURCE_CREATE_2D), - resource_id: id, - format, - width, - height, - }; - let resp = self.command(&cmd); - assert!(resp == RESP_OK_NODATA, "VirtIO GPU: RESOURCE_CREATE_2D failed: {:#x}", resp); - } - - fn destroy_resource(&mut self, id: u32) { - let cmd = ResourceUnref { - hdr: CtrlHeader::new(CMD_RESOURCE_UNREF), - resource_id: id, - padding: 0, - }; - let resp = self.command(&cmd); - assert!(resp == RESP_OK_NODATA, "VirtIO GPU: RESOURCE_UNREF failed: {:#x}", resp); - } - - fn attach_backing(&mut self, id: u32, addr: u64, len: u32) { - let resp = self.attach_backing_answering(id, addr, len); - assert!(resp == RESP_OK_NODATA, "VirtIO GPU: RESOURCE_ATTACH_BACKING failed: {:#x}", resp); - } - - fn attach_backing_answering(&mut self, id: u32, addr: u64, len: u32) -> u32 { - // Variable-length payload: header followed by mem_entry array, written - // consecutively. - let cmd = ResourceAttachBacking { - hdr: CtrlHeader::new(CMD_RESOURCE_ATTACH_BACKING), - resource_id: id, - nr_entries: 1, - }; - let entry = MemEntry { addr, length: len, padding: 0 }; - - let cmd_size = core::mem::size_of::(); - let entry_size = core::mem::size_of::(); - self.req.write(0, cmd); - self.req.write(cmd_size, entry); - - let slot = self.control_slot.take().expect("GPU: no control slot"); - self.control_slot = Some(self.controlq.submit_and_wait( - slot, - &[ - (self.req.device_addr(), (cmd_size + entry_size) as u32, BufDir::Readable), - (self.resp.device_addr(), core::mem::size_of::() as u32, BufDir::Writable), - ], - self.device.notify_mmio(), - self.device.notify_off_multiplier(), - 0, - )); - - self.answer::().cmd_type - } - - fn set_scanout(&mut self, scanout: u32, resource: u32, rect: Rect) { - let cmd = SetScanout { - hdr: CtrlHeader::new(CMD_SET_SCANOUT), - r: rect, - scanout_id: scanout, - resource_id: resource, - }; - let resp = self.command(&cmd); - assert!(resp == RESP_OK_NODATA, "VirtIO GPU: SET_SCANOUT failed: {:#x}", resp); - } - - fn transfer_to_host(&mut self, resource: u32, rect: Rect, offset: u64) { - let cmd = TransferToHost2d { - hdr: CtrlHeader::new(CMD_TRANSFER_TO_HOST_2D), - r: rect, - offset, - resource_id: resource, - padding: 0, - }; - let resp = self.command(&cmd); - assert!(resp == RESP_OK_NODATA, "VirtIO GPU: TRANSFER_TO_HOST_2D failed: {:#x}", resp); - } - - fn flush(&mut self, resource: u32, rect: Rect) { - let cmd = ResourceFlush { - hdr: CtrlHeader::new(CMD_RESOURCE_FLUSH), - r: rect, - resource_id: resource, - padding: 0, - }; - let resp = self.command(&cmd); - assert!(resp == RESP_OK_NODATA, "VirtIO GPU: RESOURCE_FLUSH failed: {:#x}", resp); - } - - fn cursor_command(&mut self, req: &T) { - self.cursor_req.write(0, *req); - let slot = self.cursor_slot.take().expect("GPU: no cursor slot"); - self.cursor_slot = Some(self.cursorq.submit_and_wait( - slot, - &[ - (self.cursor_req.device_addr(), core::mem::size_of::() as u32, BufDir::Readable), - (self.cursor_resp.device_addr(), core::mem::size_of::() as u32, BufDir::Writable), - ], - self.device.notify_mmio(), - self.device.notify_off_multiplier(), - 1, // cursor queue index - )); - } - - fn update_cursor(&mut self, x: u32, y: u32, hot_x: u32, hot_y: u32) { - let cmd = UpdateCursor { - hdr: CtrlHeader::new(CMD_UPDATE_CURSOR), - pos: CursorPos { scanout_id: 0, x, y, padding: 0 }, - resource_id: CURSOR_RESOURCE_ID, - hot_x, - hot_y, - padding: 0, - }; - self.cursor_command(&cmd); - } - - fn move_cursor(&mut self, x: u32, y: u32) { - let cmd = UpdateCursor { - hdr: CtrlHeader::new(CMD_MOVE_CURSOR), - pos: CursorPos { scanout_id: 0, x, y, padding: 0 }, - resource_id: CURSOR_RESOURCE_ID, - hot_x: 0, - hot_y: 0, - padding: 0, - }; - self.cursor_command(&cmd); - } - - /// `None` when the physical memory or a device address is not there: the - /// boot-time claim may panic on it, a mode change may only refuse. - fn alloc_framebuffer(&mut self, fb_size: u32) -> Option { - let fb_size = fb_size as usize; - let fb_pages = fb_size.div_ceil(PAGE_2M as usize); - let fb_aligned = (fb_pages * PAGE_2M as usize) as u64; - let all_pages = - [crate::mm::pmm::alloc_contiguous(fb_pages)?, - crate::mm::pmm::alloc_contiguous(fb_pages)?]; - let regions = all_pages.map(|pages| { - let phys = pages[0].direct_map().phys(); - Region { - phys: crate::DirectMap::from_phys(phys), - size: fb_aligned, - cache: CachePolicy::Normal, - pages: Some(alloc::sync::Arc::new(Pages::new(pages))), - } - }); - let backing = match AttachedBacking::take(self.space, regions[0].phys.phys(), fb_aligned) { - Ok(backing) => backing, - Err(why) => { - log!("VirtIO GPU: no device address for a {fb_size}-byte scanout: {why}"); - return None; - } - }; - log!( - "VirtIO GPU: scanout buffer at {:?} phys={:#x} device={:#x} ({} bytes)", - regions[0].phys.as_mut_ptr::(), - regions[0].phys.phys(), - backing.at, - fb_size - ); - Some(FbAlloc { regions, backing: Some(backing) }) - } - - fn build_gpu_info(&self) -> GpuInfo { - GpuInfo { - scanout: self.fb.regions.clone(), - cursor: self.cursor.clone(), - width: self.width, - height: self.height, - stride: self.width, - pixel_format: 1, // BGR (B8G8R8X8_UNORM) - flags: FLAG_HARDWARE_CURSOR, - edid: [0; toyos_abi::boot::EDID_BLOCK], - edid_len: 0, - } - } -} - -impl Gpu for GpuController { - fn present_rect(&mut self, x: u32, y: u32, w: u32, h: u32) { - let rect = if w == 0 || h == 0 { - Rect { x: 0, y: 0, width: self.width, height: self.height } - } else { - let cx = x.min(self.width); - let cy = y.min(self.height); - let cw = w.min(self.width - cx); - let ch = h.min(self.height - cy); - if cw == 0 || ch == 0 { return; } - Rect { x: cx, y: cy, width: cw, height: ch } - }; - let offset = (rect.y as u64 * self.width as u64 + rect.x as u64) * 4; - self.transfer_to_host(self.resource, rect, offset); - self.flush(self.resource, rect); - } - - fn set_cursor(&mut self, hot_x: u32, hot_y: u32) { - let rect = Rect { x: 0, y: 0, width: CURSOR_SIZE, height: CURSOR_SIZE }; - self.transfer_to_host(CURSOR_RESOURCE_ID, rect, 0); - self.update_cursor(0, 0, hot_x, hot_y); - } - - fn move_cursor(&mut self, x: u32, y: u32) { - GpuController::move_cursor(self, x, y); - } - - fn set_resolution(&mut self, width: u32, height: u32) -> Result { - if width == self.width && height == self.height { - return Ok(self.build_gpu_info()); - } - - let fb_size = fb_size_bytes(width, height).ok_or(SyscallError::InvalidArgument)?; - - log!("VirtIO GPU: changing resolution {}x{} -> {}x{}", self.width, self.height, width, height); - - // Old framebuffer stays live until the swap below: a failure here - // leaves the display unchanged. - let new_fb = self.alloc_framebuffer(fb_size).ok_or(SyscallError::ResourceExhausted)?; - - let old_resource = self.resource; - self.resource += 1; - self.create_resource(self.resource, FORMAT_B8G8R8X8_UNORM, width, height); - self.attach_backing(self.resource, new_fb.device_addr(), fb_size); - - let rect = Rect { x: 0, y: 0, width, height }; - self.set_scanout(0, self.resource, rect); - - // Acknowledged on the control queue's used ring, so the device is off the - // old backing before its address stops translating below. - self.destroy_resource(old_resource); - // Old pages free only when the last holder drops them; the attachment - // ends here whatever holds them. - drop(core::mem::replace(&mut self.fb, new_fb)); - - self.width = width; - self.height = height; - - log!("VirtIO GPU: resolution set to {}x{}", width, height); - - Ok(self.build_gpu_info()) - } -} - -/// Byte size for a resolution, or `None` if zero or too large to fit `u32`. -fn fb_size_bytes(width: u32, height: u32) -> Option { - // u64: width * height * 4 can overflow u32. - let bytes = (width as u64).checked_mul(height as u64)?.checked_mul(4)?; - if bytes == 0 || bytes > u32::MAX as u64 { - return None; - } - Some(bytes as u32) -} - -/// Probes for the VirtIO GPU; `None` if absent. -pub fn init(devices: &[PciDevice]) -> Option<(Box, GpuInfo)> { - let pci_dev = *devices.iter().find(|d| d.is_id(VIRTIO_VENDOR, VIRTIO_GPU_DEVICE))?; - log!("VirtIO GPU: found at PCI {:02x}:{:02x}.{}", pci_dev.bus, pci_dev.dev, pci_dev.func); - let space = DeviceSpace::create(&[pci_dev]); - // Leaked rather than held in a `static`: the display is never unbound. - let dma = DmaPool::alloc_in(DMA_SIZE, space).leak(); - // Before the device is told any address, and after the command pool's mapping. - space.attach(pci_dev.bus, pci_dev.dev, pci_dev.func); - - let device = match VirtioDevice::init(&pci_dev, VIRTIO_F_VERSION_1 | VIRTIO_GPU_F_EDID) { - Ok(device) => device, - Err(why) => { - log!("VirtIO GPU: PCI {:02x}:{:02x}.{} {why} — device refused", - pci_dev.bus, pci_dev.dev, pci_dev.func); - return None; - } - }; - - let mut controlq = Virtqueue::new(dma.subview(OFF_CONTROLQ, 0x1000), 16); - let mut cursorq = Virtqueue::new(dma.subview(OFF_CURSORQ, 0x1000), 16); - - device.setup_queue(0, &mut controlq); - device.setup_queue(1, &mut cursorq); - device.enable_queue(0); - device.enable_queue(1); - device.activate(); - - let mut control_slots = controlq.initial_slots(); - let mut cursor_slots = cursorq.initial_slots(); - let control_slot = control_slots.pop().expect("GPU: no control slots"); - let cursor_slot = cursor_slots.pop().expect("GPU: no cursor slots"); - drop(control_slots); - drop(cursor_slots); - - let ctrl_bufs = dma.subview(OFF_CONTROLQ_BUFS, 0x1000); - let cursor_bufs = dma.subview(OFF_CURSORQ_BUFS, 0x1000); - // Each half of a buffer page: request at 0, response at `RESP_OFFSET`. - const HALF: usize = RESP_OFFSET - REQ_OFFSET; - - let mut gpu = GpuController { - device, - space, - controlq, - cursorq, - control_slot: Some(control_slot), - cursor_slot: Some(cursor_slot), - req: ctrl_bufs.subview(REQ_OFFSET, HALF).unaligned(), - resp: ctrl_bufs.subview(RESP_OFFSET, HALF), - cursor_req: cursor_bufs.subview(REQ_OFFSET, HALF).unaligned(), - cursor_resp: cursor_bufs.subview(RESP_OFFSET, HALF), - width: 0, - height: 0, - resource: FIRST_SCANOUT_RESOURCE_ID, - fb: FbAlloc { regions: core::array::from_fn(|_| Region::empty()), backing: None }, - cursor: Region::empty(), - cursor_backing: None, - }; - - // EDID's reported resolution is often a stale firmware default; the - // preferred mode is the first Detailed Timing Descriptor. - let edid = gpu.get_edid(0); - let (width, height) = if edid.hdr.cmd_type == RESP_OK_EDID { - let dtd = &edid.edid[54..72]; - let w = dtd[2] as u32 | ((dtd[4] as u32 >> 4) << 8); - let h = dtd[5] as u32 | ((dtd[7] as u32 >> 4) << 8); - if w >= 1280 && h >= 720 { - (w, h) - } else { - (1280, 720) // EDID reports stale firmware resolution, use default - } - } else { - (1280, 720) - }; - log!("VirtIO GPU: display {}x{}", width, height); - - // Boot-time: failure here means the machine can't run, so it dies loudly. - let fb_size = fb_size_bytes(width, height).expect("VirtIO GPU: nonsense display dimensions"); - gpu.fb = gpu.alloc_framebuffer(fb_size).expect("VirtIO GPU: framebuffer alloc failed"); - - gpu.create_resource(gpu.resource, FORMAT_B8G8R8X8_UNORM, width, height); - gpu.attach_backing(gpu.resource, gpu.fb.device_addr(), fb_size); - - let rect = Rect { x: 0, y: 0, width, height }; - gpu.set_scanout(0, gpu.resource, rect); - - let cursor_bytes = (CURSOR_SIZE * CURSOR_SIZE * 4) as usize; - let cursor_pages = crate::mm::pmm::alloc_contiguous(1).expect("VirtIO GPU: cursor alloc failed"); - let cursor_ptr = cursor_pages[0].direct_map().as_mut_ptr::(); - let cursor_phys = cursor_pages[0].direct_map().phys(); - gpu.cursor = Region { - phys: crate::DirectMap::from_phys(cursor_phys), - size: PAGE_2M, - cache: CachePolicy::Normal, - pages: Some(alloc::sync::Arc::new(Pages::new(cursor_pages))), - }; - let cursor_backing = AttachedBacking::take(space, cursor_phys, PAGE_2M) - .unwrap_or_else(|why| panic!("VirtIO GPU: cursor mapping failed: {why}")); - gpu.create_resource(CURSOR_RESOURCE_ID, FORMAT_B8G8R8A8_UNORM, CURSOR_SIZE, CURSOR_SIZE); - gpu.attach_backing(CURSOR_RESOURCE_ID, cursor_backing.at, cursor_bytes as u32); - log!( - "VirtIO GPU: cursor resource at {:?} phys={:#x} device={:#x}", - cursor_ptr, - cursor_phys, - cursor_backing.at - ); - gpu.cursor_backing = Some(cursor_backing); - - gpu.width = width; - gpu.height = height; - - let info = gpu.build_gpu_info(); - - Some((Box::new(gpu), info)) -} diff --git a/kernel/src/gpu.rs b/kernel/src/gpu.rs deleted file mode 100644 index e0197b70861..00000000000 --- a/kernel/src/gpu.rs +++ /dev/null @@ -1,121 +0,0 @@ -use alloc::boxed::Box; -use toyos_abi::syscall::SyscallError; -use crate::object::shm::Region; -use crate::sync::Lock; - -pub const FLAG_HARDWARE_CURSOR: u32 = 1 << 0; - -/// Physical scanout/cursor ranges and their memory types, not handles: a fresh shared-memory object is minted over each on every claim. -pub struct GpuInfo { - pub scanout: [Region; 2], - pub cursor: Region, - pub width: u32, - pub height: u32, - pub stride: u32, - pub pixel_format: u32, - pub flags: u32, - /// The panel's EDID base block the loader handed over, and its length: - /// [`toyos_abi::boot::EDID_BLOCK`] or zero. - pub edid: [u8; toyos_abi::boot::EDID_BLOCK], - pub edid_len: u32, -} - -/// Hardware-agnostic GPU interface; register an implementation with `gpu::register()`. -pub trait Gpu: Send { - fn present_rect(&mut self, x: u32, y: u32, w: u32, h: u32); - fn set_cursor(&mut self, hot_x: u32, hot_y: u32); - fn move_cursor(&mut self, x: u32, y: u32); - /// Width/height are unvalidated userspace values; must refuse a resolution it cannot back rather than panic. - fn set_resolution(&mut self, width: u32, height: u32) -> Result; -} - -static GPU: Lock>> = Lock::new(None); -static INFO: Lock> = Lock::new(None); - -/// Derives the `Screen` the rest of the kernel sees from the current `GpuInfo`. -pub fn screen(info: &GpuInfo) -> crate::device::Screen { - crate::device::Screen { - // Handles are minted per claim by `try_claim`; the process reading this doesn't exist yet. - info: toyos_abi::FramebufferInfo { - scanout: [toyos_abi::HANDLE_INVALID; 2], - cursor: toyos_abi::HANDLE_INVALID, - width: info.width, - height: info.height, - stride: info.stride, - pixel_format: info.pixel_format, - flags: info.flags, - edid: info.edid, - edid_len: info.edid_len, - }, - scanout: info.scanout.clone(), - cursor: info.cursor.clone(), - } -} - -pub fn register(gpu: Box, info: GpuInfo) { - crate::device::set_framebuffer_info(screen(&info)); - *INFO.lock() = Some(info); - *GPU.lock() = Some(gpu); -} - -pub fn present_rect(x: u32, y: u32, w: u32, h: u32) { - let (x, y, w, h) = { - let info = INFO.lock(); - let Some(info) = info.as_ref() else { return }; - let x = x.min(info.width); - let y = y.min(info.height); - let w = w.min(info.width.saturating_sub(x)); - let h = h.min(info.height.saturating_sub(y)); - (x, y, w, h) - }; - if w == 0 || h == 0 { return; } - if let Some(gpu) = GPU.lock().as_mut() { - gpu.present_rect(x, y, w, h); - } -} - -pub fn set_cursor(hot_x: u32, hot_y: u32) { - if let Some(gpu) = GPU.lock().as_mut() { - gpu.set_cursor(hot_x, hot_y); - } -} - -/// Publishes the new mode as `INFO` only: the registry is the caller's single writer. -pub fn set_resolution(width: u32, height: u32) -> Result { - let new_info = { - let mut gpu = GPU.lock(); - let gpu = gpu.as_mut().ok_or(SyscallError::NotSupported)?; - // The driver may free the old framebuffer; blind the panic console so it never writes into reallocated memory. - crate::drivers::panic_console::detach(); - let result = gpu.set_resolution(width, height); - if result.is_err() { - crate::drivers::panic_console::rearm(); - } - result? - }; - *INFO.lock() = Some(GpuInfo { - scanout: new_info.scanout.clone(), - cursor: new_info.cursor.clone(), - width: new_info.width, - height: new_info.height, - stride: new_info.stride, - pixel_format: new_info.pixel_format, - flags: new_info.flags, - edid: new_info.edid, - edid_len: new_info.edid_len, - }); - Ok(new_info) -} - -pub fn move_cursor(x: u32, y: u32) { - let (max_x, max_y) = { - let info = INFO.lock(); - match info.as_ref() { - Some(i) => (i.width, i.height), - None => return, - } - }; - if let Some(gpu) = GPU.lock().as_mut() { - gpu.move_cursor(x.min(max_x), y.min(max_y)); - } -} diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 438bd0a5c5b..2a20954d653 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -88,7 +88,6 @@ mod pipe; mod device; mod pcidev; mod isa; -mod gpu; mod user_ptr; mod vma; mod syscall; @@ -108,7 +107,7 @@ mod late_panic { use alloc::boxed::Box; use arch::{cpu, percpu}; -use drivers::{acpi, gop, pci, serial, virtio_console, virtio_gpu, xhci}; +use drivers::{acpi, gop, pci, serial, virtio_console, xhci}; use toyos_abi::boot::{KernelArgs, MemoryMapEntry}; use toyos_rootimage::handoff::{held, Descriptor}; @@ -179,10 +178,6 @@ fn gop_edid(args: &KernelArgs) -> ([u8; toyos_abi::boot::EDID_BLOCK], u32) { } } -fn register_gpu(driver: Box, info: gpu::GpuInfo) { - gpu::register(driver, info); -} - /// The boot from power-on, off the loader's [`cpu::counter`] readings and /// `complete`'s, at the clock's rate. The first span is firmware's time since /// the counter started. @@ -535,12 +530,7 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { drivers::hda::init(&pci_devices); - if let Some((gpu_driver, gpu_info)) = virtio_gpu::init(&pci_devices) { - log!("GPU: using VirtIO"); - // virtio's scanout is only reachable through a virtqueue round trip behind GPU.lock(), which the panic path may not take. - drivers::panic_console::disable(); - register_gpu(gpu_driver, gpu_info); - } else if kernel_args.gop_framebuffer != 0 { + if kernel_args.gop_framebuffer != 0 { log!("GPU: using UEFI GOP"); match gop::init( kernel_args.gop_framebuffer, @@ -551,11 +541,11 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { kernel_args.gop_pixel_format, gop_edid(kernel_args), ) { - Some((gpu_driver, gpu_info)) => register_gpu(gpu_driver, gpu_info), + Some(screen) => device::set_framebuffer_info(screen), None => log!("GPU: none this boot, running headless"), } } else { - log!("GPU: none found, running headless"); + log!("GPU: the firmware handed over no framebuffer"); } boot_phase!("devices ready", t_devices); diff --git a/kernel/src/object/device.rs b/kernel/src/object/device.rs index e9833b293d0..841e3855cd9 100644 --- a/kernel/src/object/device.rs +++ b/kernel/src/object/device.rs @@ -19,7 +19,8 @@ use super::{Held, KObjectRef, KObjectVariant, ObjectCore, ZeroHandles}; pub enum DeviceInfo { // Keyboard and mouse answer with events, not a description. Events, - Framebuffer(FramebufferInfo, FramebufferBuffers), + /// 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), @@ -33,12 +34,6 @@ pub enum DeviceInfo { Acpi(toyos_abi::acpi::AcpiInfo), } -/// The two scanout buffers and the cursor plane. -pub struct FramebufferBuffers { - pub scanout: [Arc; 2], - pub cursor: Arc, -} - // MAP is required; DUP and TRANSFER let a daemon hand the buffer to another process. const BUFFER_RIGHTS: Rights = Rights::MAP.union(Rights::DUP).union(Rights::TRANSFER); @@ -60,14 +55,9 @@ impl DeviceInfo { fn mint(&self, table: &mut HandleTable) -> Result, SyscallError> { Ok(match self { Self::Events => Box::new([]), - Self::Framebuffer(info, buffers) => { + Self::Framebuffer(info, scanout) => { let mut info = *info; - let h = install_buffers( - table, - &[&buffers.scanout[0], &buffers.scanout[1], &buffers.cursor], - )?; - info.scanout = [h[0], h[1]]; - info.cursor = h[2]; + info.scanout = install_buffers(table, &[scanout])?[0]; bytes(&info).into() } // Nothing to install: every address in it is a size, and the memory @@ -98,7 +88,6 @@ pub struct DeviceClaim { // Minted once: re-minting on a second read would leak a handle each time. struct Described { info: DeviceInfo, - // Shares this lock with `info`: SYS_GPU_SET_RESOLUTION must replace both together. bytes: Option>, } @@ -192,45 +181,26 @@ impl DeviceClaim { self.info_read.store(true, Ordering::Relaxed); count as u64 } - - /// Replace the description, for a mode set that reallocated the buffers — old handles keep working; nothing is revoked. - pub fn remint( - &self, - table: &mut HandleTable, - info: DeviceInfo, - ) -> Result, SyscallError> { - let mut described = self.described.lock(); - let minted = info.mint(table)?; - described.info = info; - described.bytes = Some(minted.clone()); - Ok(minted) - } } -/// Negative control for the mint's all-or-nothing rule: a three-handle -/// framebuffer batch minted into a table with room for two must install none. +/// Negative control for the mint's all-or-nothing rule: a framebuffer +/// description minted into a table with no room installs nothing. #[cfg(feature = "boot-actuators")] pub(crate) fn mint_rollback_selftest() { use crate::object::shm::{Region, SharedMemObject}; let mut table = HandleTable::new(); - table.stage_room(2); - let buffers = FramebufferBuffers { - scanout: [SharedMemObject::over(Region::empty()), SharedMemObject::over(Region::empty())], - cursor: SharedMemObject::over(Region::empty()), - }; + table.stage_room(0); let info = DeviceInfo::Framebuffer( FramebufferInfo { - scanout: [toyos_abi::HANDLE_INVALID; 2], - cursor: toyos_abi::HANDLE_INVALID, + scanout: toyos_abi::HANDLE_INVALID, width: 0, height: 0, stride: 0, pixel_format: 0, - flags: 0, edid: [0; toyos_abi::boot::EDID_BLOCK], edid_len: 0, }, - buffers, + SharedMemObject::over(Region::empty()), ); let before = table.iter().count(); let refused = info.mint(&mut table).is_err(); diff --git a/kernel/src/object/shm.rs b/kernel/src/object/shm.rs index 0f58e678174..8e935f49dee 100644 --- a/kernel/src/object/shm.rs +++ b/kernel/src/object/shm.rs @@ -29,12 +29,6 @@ const _: () = assert!(toyos_abi::syscall::SHM_PAGE as u64 == PAGE_2M); #[expect(dead_code)] pub struct Pages(Vec); -impl Pages { - pub fn new(pages: Vec) -> Self { - Self(pages) - } -} - /// A physical range and the memory type every mapping of it must carry: /// SDM Vol. 3A §11.12.4 forbids one physical page under two memory types. #[derive(Clone)] @@ -48,8 +42,9 @@ pub struct Region { } impl Region { - /// A placeholder for a driver struct built before its buffers exist: - /// size zero maps nothing. + /// Size zero, which maps nothing: a description's buffer where a control + /// needs one and no pages. + #[cfg(feature = "boot-actuators")] pub fn empty() -> Self { Self { phys: DirectMap::from_phys(0), size: 0, cache: CachePolicy::Normal, pages: None } } diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index 06c541f6116..3c10cd8824d 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -14,7 +14,7 @@ //! claim. //! //! **What a holder lends is placed in a window of its function's domain -//! reserved for lending, [`MAX_GRANT_TOTAL`] long, and nowhere else**, so +//! reserved for lending, [`LEND_WINDOW`] long, and nowhere else**, so //! lending and taking back as often as a holder likes spends no address and no //! remapping table past that window's, and a window with no room left is a //! refusal ([`SyscallError::ResourceExhausted`]). An address in it is lent @@ -123,7 +123,7 @@ use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use record::Interrupt; use toyos_abi::boot::{MemoryMapEntry, RootBridgeWindow}; -use toyos_abi::pci::{DeviceIrqRecord, PciFunctionInfo, BARS}; +use toyos_abi::pci::{DeviceIrqRecord, PciFunctionInfo, BARS, LEND_WINDOW}; use toyos_abi::syscall::{PciId, RegWidth, SyscallError}; use toyos_dma::Register; use toyos_pci::bridge::Window; @@ -145,8 +145,8 @@ use crate::watch::IrqWatch; /// memory. const MAX_GRANT_BYTES: u64 = 8 * 1024 * 1024; -/// And the most one claim may hold across every grant, and the length of the -/// window its lent regions are placed in ([`Space::lend`]). +/// And the most one claim may hold across every grant. What it lends is +/// bounded apart, by the window it is placed in ([`Space::lend`]). const MAX_GRANT_TOTAL: u64 = 32 * 1024 * 1024; /// The unit every run in this module's records is said in. @@ -222,7 +222,7 @@ fn function(number: usize) -> &'static Function { #[derive(Clone, Copy)] struct Space { space: OwnSpace, - /// [`MAX_GRANT_TOTAL`] of addresses handed out with the domain and never + /// [`LEND_WINDOW`] of addresses handed out with the domain and never /// mapped by anything but [`dma_map`]. lend: u64, } @@ -311,9 +311,9 @@ impl Bound { let residue = self.residue.iter().map(|aimed| (aimed.at, aimed.bytes)); grants.chain(residue).any(|(from, bytes)| from < end && at < from + bytes) }; - (0..MAX_GRANT_TOTAL / PAGE_2M) + (0..LEND_WINDOW / PAGE_2M) .map(|leaf| self.lend + leaf * PAGE_2M) - .take_while(|at| at + span <= self.lend + MAX_GRANT_TOTAL) + .take_while(|at| at + span <= self.lend + LEND_WINDOW) .find(|at| !taken(*at)) } @@ -995,7 +995,7 @@ fn function_space(number: usize) -> Result { match *held { Some(space) => Ok(space), None => { - let (space, lend) = OwnSpace::create(MAX_GRANT_TOTAL)?; + let (space, lend) = OwnSpace::create(LEND_WINDOW)?; *held = Some(Space { space, lend }); Ok(Space { space, lend }) } @@ -1625,7 +1625,12 @@ pub fn dma_alloc( if bytes == 0 || bytes > MAX_GRANT_BYTES { return Err(SyscallError::InvalidArgument); } - let held: u64 = bound.grants.iter().map(|grant| grant.bytes).sum(); + let held: u64 = bound + .grants + .iter() + .filter(|grant| grant.origin != Origin::Mapped) + .map(|grant| grant.bytes) + .sum(); let span = align_2m(bytes as usize) as u64; if held + span > MAX_GRANT_TOTAL { return Err(SyscallError::ResourceExhausted); @@ -1699,9 +1704,8 @@ pub fn dma_undo(binding: &Binding, memory: &Arc) { /// would be one device aimed at another's registers, and firmware's /// framebuffer is not the kernel's to lend. /// -/// Counted against [`MAX_GRANT_TOTAL`] with the claim's own grants, so a holder -/// mapping every region its clients send is bounded by the same number; and a -/// window with no run of the region's length left free is the same refusal. +/// Bounded by [`LEND_WINDOW`] alone: a window with no run of the region's +/// length left free is a refusal. pub fn dma_map(binding: &Binding, region: &Arc) -> Result<(u64, u64), SyscallError> { let function = binding.function(); with_bound(binding, |bound| { @@ -1712,10 +1716,6 @@ pub fn dma_map(binding: &Binding, region: &Arc) -> Result<(u64, if bound.grants.iter().any(|grant| Arc::ptr_eq(&grant.memory, region)) { return Err(SyscallError::InvalidArgument); } - let held: u64 = bound.grants.iter().map(|grant| grant.bytes).sum(); - if held + span > MAX_GRANT_TOTAL { - return Err(SyscallError::ResourceExhausted); - } let at = bound.lend_room(span).ok_or(SyscallError::ResourceExhausted)?; // Inside room the domain handed out with the function, and the pages are // the allocator's own whole 2 MiB ones: a refusal here is a kernel bug. diff --git a/kernel/src/syscall/device.rs b/kernel/src/syscall/device.rs index fa5ed5a203c..0aa432e984c 100644 --- a/kernel/src/syscall/device.rs +++ b/kernel/src/syscall/device.rs @@ -285,9 +285,8 @@ pub(super) fn sys_device_dma_alloc( Ok(claim) => claim, Err(e) => return e.refuse(), }; - // The output window is taken before the allocation, the same order - // `sys_gpu_reset_scanout` takes it in: a bad address must not leave a - // mapped grant the caller was never told the address of. + // The output window is taken before the allocation: a bad address must + // not leave a mapped grant the caller was never told the address of. let len = core::mem::size_of::() as u64; let Some(mut window) = ctx.user_bytes_mut(out, len) else { return SyscallError::BadAddress.to_u64(); @@ -400,53 +399,6 @@ fn grant_bytes(grant: &toyos_abi::pci::DmaGrant) -> &[u8] { } } -/// Publishes a new mode with fresh buffer handles; the claim's old handles keep working. -pub(super) fn sys_gpu_reset_scanout( - ctx: &SyscallContext, - claim_h: RawHandle, - gpu_info: crate::gpu::GpuInfo, - info_out: UserAddr, -) -> u64 { - let crate::gpu::GpuInfo { scanout, cursor, width, height, stride, pixel_format, flags, edid, edid_len } = - gpu_info; - let screen = device::Screen { - info: toyos_abi::FramebufferInfo { - scanout: [toyos_abi::HANDLE_INVALID; 2], - cursor: toyos_abi::HANDLE_INVALID, - width, - height, - stride, - pixel_format, - flags, - edid, - edid_len, - }, - scanout, - cursor, - }; - device::set_framebuffer_info(screen.clone()); - // The output window is taken before the mint: a bad address must not leave a - // resolution, or fresh buffer handles, the caller was never handed. Length is `FramebufferInfo`'s. - let len = core::mem::size_of::() as u64; - let Some(mut out) = ctx.user_bytes_mut(info_out, len) else { - return SyscallError::BadAddress.to_u64(); - }; - let minted = process::with_process_data(|data| { - let claim = data - .handles - .get::(claim_h, Rights::WRITE)?; - Ok::<_, crate::object::Refusal>( - claim.remint(&mut data.handles, device::framebuffer_info(screen))?, - ) - }); - let minted = match minted { - Ok(bytes) => bytes, - Err(e) => return e.refuse(), - }; - out.write_at(0, &minted); - 0 -} - /// 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>), diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index 0a5908ff84e..db1b8c22e9d 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -11,7 +11,7 @@ use crate::object::{ops, KObjectRef}; use crate::user_ptr::SyscallContext; use crate::UserAddr; -use crate::{device, process}; +use crate::process; use toyos_abi::handle::{RawHandle, Rights}; #[cfg(feature = "test-actuators")] @@ -23,9 +23,9 @@ use super::HANDLE_LEN; #[cfg(feature = "test-actuators")] use super::debug::{canary, debug_heap_alloc, ring0_timer_in_syscall, FATAL_HALT_NONCE, LOCK_ACROSS_SWITCH}; use super::device::{ - holds_claim, sys_acpi, sys_hda_bind, sys_device_bar_map, sys_device_claim, sys_device_dma_alloc, sys_device_dma_map, + sys_acpi, sys_hda_bind, sys_device_bar_map, sys_device_claim, sys_device_dma_alloc, sys_device_dma_map, sys_device_dma_unmap, - sys_device_reg, sys_gpu_reset_scanout, sys_partition_transfer, Transfer, + sys_device_reg, sys_partition_transfer, Transfer, }; use super::fs::{ spawn_cwd, sys_chdir, sys_delete, sys_getcwd, sys_mkdir, sys_open, sys_readdir, sys_readlink, @@ -252,21 +252,6 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> // No right: the one caller marks both ends of a pair, so requiring one would refuse the other. SYS_MARK_TTY => with_object(RawHandle(a1 as u32), Rights::NONE, ops::mark_tty), - // Gated for display integrity, not memory access: contents are already behind shared_memory grants. - // Ungated, any process could scan over the compositor's frames or move its cursor. - SYS_GPU_PRESENT | SYS_GPU_SET_CURSOR | SYS_GPU_MOVE_CURSOR => { - if let Err(e) = holds_claim(RawHandle(a1 as u32), device::DeviceType::Framebuffer) { - return e.refuse(); - } - let (hi2, lo2) = unpair(a2); - let (hi3, lo3) = unpair(a3); - match num { - SYS_GPU_PRESENT => crate::gpu::present_rect(hi2, lo2, hi3, lo3), - SYS_GPU_SET_CURSOR => crate::gpu::set_cursor(a2 as u32, a3 as u32), - _ => crate::gpu::move_cursor(a2 as u32, a3 as u32), - } - 0 - } SYS_THREAD_SPAWN => sys_thread_spawn(a1, a2, a3, a4), SYS_THREAD_JOIN => sys_thread_join(a1), // Both answer from the boot-time anchor, never the CMOS: NotSupported beats a @@ -422,24 +407,6 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> let Some(mut buf) = ctx.user_bytes_mut(UserAddr::new(a3), a4) else { return bad_addr }; sys_readlink(&path, &mut buf) } - SYS_GPU_SET_RESOLUTION => { - // Checked before the driver, so an unclaimed caller never turns two - // arbitrary u32s into a physical allocation. - let claim_h = RawHandle(a1 as u32); - if let Err(e) = holds_claim(claim_h, device::DeviceType::Framebuffer) { - return e.refuse(); - } - // Checked before the allocation, same reason as the claim: a bad address - // must not leave a resolution the caller was never told about. - let Some(info_out) = UserAddr::checked(a3) else { return bad_addr }; - let (width, height) = unpair(a2); - match crate::gpu::set_resolution(width, height) { - // ctx lets sys_gpu_reset_scanout mint the buffers before it knows the - // write-back length. - Ok(gpu_info) => sys_gpu_reset_scanout(&ctx, claim_h, gpu_info, info_out), - Err(e) => e.to_u64(), - } - } SYS_ENDOWMENTS => { let Some(mut buf) = ctx.user_bytes_mut(UserAddr::new(a1), a2) else { return bad_addr }; sys_endowments(&mut buf) @@ -704,8 +671,3 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> result } - -/// The two `u32`s a device call packs into one argument word, taken apart here. -fn unpair(word: u64) -> (u32, u32) { - ((word >> 32) as u32, word as u32) -} diff --git a/lib/virtio/src/pci.rs b/lib/virtio/src/pci.rs index 642ec8e0e55..2abff7bdd51 100644 --- a/lib/virtio/src/pci.rs +++ b/lib/virtio/src/pci.rs @@ -355,6 +355,16 @@ impl Wires { self.set_status(status::FAILED); why } + + /// Where in the BAR the 32-bit device-specific field `at` is, read or + /// written 32 bits wide as §4.1.3.1 has one. + fn device32(&mut self, at: usize) -> Result { + let Region { at: base, bytes } = self.device; + if at.checked_add(4).is_none_or(|end| end > bytes) || !at.is_multiple_of(4) { + return Err(self.refuse(Refusal::PastDeviceConfig { at, bytes })); + } + Ok(base + at) + } } /// A device reset and acknowledged, and the features it offers. @@ -519,31 +529,42 @@ impl Setup { Ok((self, byte)) } - /// The 32-bit field `at` bytes into the device-specific structure, read - /// 32 bits wide as §4.1.3.1 has a 32-bit field read. + /// The 32-bit field `at` bytes into the device-specific structure. pub fn device_read32(mut self, at: usize) -> Result<(Self, u32), Refusal> { - let Region { at: base, bytes } = self.wires.device; - if at.checked_add(4).is_none_or(|end| end > bytes) || !at.is_multiple_of(4) { - return Err(self.wires.refuse(Refusal::PastDeviceConfig { at, bytes })); - } - let word = self.wires.regs.read32(base + at); + let at = self.wires.device32(at)?; + let word = self.wires.regs.read32(at); Ok((self, word)) } /// §3.1.1 step 8: the device is live. pub fn driver_ok(mut self) -> Live { self.wires.set_status(status::DRIVER_OK); - Live { regs: self.wires.regs, doorbells: self.doorbells } + Live { wires: self.wires, doorbells: self.doorbells } } } -/// A live device: what is left of the transport is its notify addresses. +/// A live device: what is left of the transport is its notify addresses and +/// its device-specific structure. pub struct Live { - regs: R, + wires: Wires, doorbells: Vec<(u16, usize)>, } impl Live { + /// The 32-bit field `at` bytes into the device-specific structure. A + /// refusal sets `FAILED`, as one before `DRIVER_OK` does. + pub fn device_read32(&mut self, at: usize) -> Result { + let at = self.wires.device32(at)?; + Ok(self.wires.regs.read32(at)) + } + + /// Write the 32-bit field `at` bytes into the device-specific structure. + pub fn device_write32(&mut self, at: usize, value: u32) -> Result<(), Refusal> { + let at = self.wires.device32(at)?; + self.wires.regs.write32(at, value); + Ok(()) + } + /// Tell the device of the chain `published` made available: the queue's /// index, sixteen bits, at its notify address (§4.1.5.2). /// @@ -557,6 +578,6 @@ impl Live { .iter() .find(|(index, _)| *index == queue) .unwrap_or_else(|| panic!("virtio: queue {queue} was published to and never enabled")); - self.regs.write16(*at, queue); + self.wires.regs.write16(*at, queue); } } diff --git a/lib/virtio/src/stub.rs b/lib/virtio/src/stub.rs index 1a96a506701..0b826574eff 100644 --- a/lib/virtio/src/stub.rs +++ b/lib/virtio/src/stub.rs @@ -58,6 +58,9 @@ pub enum Event { Common { field: usize, value: u32 }, /// A read of the device-specific structure. DeviceConfig { at: usize }, + /// A write of the device-specific structure, whose fields are the device + /// type's to make writable. + DeviceConfigWrite { at: usize, value: u32 }, /// A store into the grant. Store { at: usize, bytes: usize, value: u64 }, /// A load from the grant. @@ -364,9 +367,13 @@ impl State { let value = write.expect("stub: a read of the notification structure"); self.notify(at - NOTIFY_AT as usize, bytes, value); } else if device.contains(&at) { - assert!(write.is_none(), "stub: the device structure is read and never written"); let at = at - DEVICE_AT as usize; - assert!(at + bytes <= self.device_config.len(), "stub: a read past the device structure"); + assert!(at + bytes <= self.device_config.len(), "stub: an access past the device structure"); + if let Some(value) = write { + self.trace.push(Event::DeviceConfigWrite { at, value }); + self.device_config[at..at + bytes].copy_from_slice(&value.to_le_bytes()[..bytes]); + return 0; + } self.trace.push(Event::DeviceConfig { at }); let mut word = [0u8; 4]; word[..bytes].copy_from_slice(&self.device_config[at..at + bytes]); diff --git a/lib/virtio/src/tests.rs b/lib/virtio/src/tests.rs index 5fc3de419a0..b437035d020 100644 --- a/lib/virtio/src/tests.rs +++ b/lib/virtio/src/tests.rs @@ -458,6 +458,30 @@ fn a_32_bit_field_is_read_32_bits_wide_and_only_inside_the_structure() { } } +/// A live device's own fields: read and written 32 bits wide inside the +/// structure, and refused, with `FAILED`, outside it. +#[test] +fn a_live_device_reads_and_writes_its_own_fields_inside_the_structure() { + let machine = Machine::new(); + let (mut device, _queue) = live(&machine); + machine.forget_trace(); + assert_eq!(device.device_read32(0), Ok(0x1200_5452)); + assert_eq!(device.device_write32(0, 0xA5A5_0001), Ok(())); + assert_eq!( + machine.trace(), + [Event::DeviceConfig { at: 0 }, Event::DeviceConfigWrite { at: 0, value: 0xA5A5_0001 }] + ); + for at in [8, 4, 2, usize::MAX - 1] { + let machine = Machine::new(); + let (mut device, _queue) = live(&machine); + machine.forget_trace(); + assert_eq!(device.device_read32(at), Err(Refusal::PastDeviceConfig { at, bytes: 6 })); + assert_eq!(device.device_write32(at, 1), Err(Refusal::PastDeviceConfig { at, bytes: 6 })); + let failed = status::ACKNOWLEDGE | status::DRIVER | status::FEATURES_OK | status::DRIVER_OK | status::FAILED; + assert_eq!(machine.trace(), [Event::Status(failed), Event::Status(failed)]); + } +} + // --- PCI 3.0 §6.7: the capability list --- /// A function's configuration space: 256 bytes, and the offsets a claim diff --git a/sdk/abi/src/lib.rs b/sdk/abi/src/lib.rs index 4a49aaf5b69..3dcbbee12d8 100644 --- a/sdk/abi/src/lib.rs +++ b/sdk/abi/src/lib.rs @@ -75,22 +75,18 @@ impl core::ops::Add for Tid { } crate::user_safe! { - /// GPU framebuffer info passed between kernel and userland. - /// Shared definition so both sides agree on the layout. + /// The firmware's framebuffer, as its claim describes it. /// - /// **The three handles are installed by the read that answers this.** A - /// description is a set of buffers, and the process being told about them is - /// the one that must be able to map them — which is never the process that - /// minted the claim, because the supervisor mints every claim and holds none. + /// **The scanout handle is installed by the read that answers this**, in + /// the process that reads it: the supervisor mints every claim and holds + /// none. #[derive(Clone, Copy)] pub struct FramebufferInfo { - pub scanout: [RawHandle; 2], - pub cursor: RawHandle, + pub scanout: RawHandle, pub width: u32, pub height: u32, pub stride: u32, pub pixel_format: u32, - pub flags: u32, /// The panel's EDID base block as the loader handed it over, unparsed, /// and how many of these bytes it is: [`boot::EDID_BLOCK`] or zero. pub edid: [u8; boot::EDID_BLOCK], diff --git a/sdk/abi/src/pci.rs b/sdk/abi/src/pci.rs index 2e22bb2965c..776382fd82c 100644 --- a/sdk/abi/src/pci.rs +++ b/sdk/abi/src/pci.rs @@ -111,3 +111,8 @@ impl DeviceIrqRecord { } const _: () = assert!(DeviceIrqRecord::SIZE == 4 + 4 + 8 + 8); + +/// How many bytes of regions one claim may have lent its function at once +/// ([`crate::syscall::device_dma_map`]), apart from its own grants: a window +/// of the function's address space, so a lend costs addresses and no memory. +pub const LEND_WINDOW: u64 = 256 * 1024 * 1024; diff --git a/sdk/abi/src/syscall.rs b/sdk/abi/src/syscall.rs index 0514d57bd49..f4012caa0b7 100644 --- a/sdk/abi/src/syscall.rs +++ b/sdk/abi/src/syscall.rs @@ -20,12 +20,9 @@ pub const SYS_PIPE: u64 = 24; /// `Process` handle — see [`spawn`]. pub const SYS_SPAWN: u64 = 25; pub const SYS_MARK_TTY: u64 = 28; -pub const SYS_GPU_PRESENT: u64 = 35; pub const SYS_THREAD_SPAWN: u64 = 40; pub const SYS_THREAD_JOIN: u64 = 41; pub const SYS_CLOCK_REALTIME: u64 = 42; -pub const SYS_GPU_SET_CURSOR: u64 = 43; -pub const SYS_GPU_MOVE_CURSOR: u64 = 44; /// A header describing the machine, optionally followed by the roster of every /// live thread in it. See [`sysinfo`]. /// @@ -114,7 +111,6 @@ pub const SYS_CONNECTION_JOIN: u64 = 76; pub const SYS_PIPE_MAP: u64 = 77; pub const SYS_SYMLINK: u64 = 81; pub const SYS_READLINK: u64 = 82; -pub const SYS_GPU_SET_RESOLUTION: u64 = 83; /// Accept a queued connection from an [`Acceptor`] handle. /// /// [`Acceptor`]: crate::handle::RawHandle @@ -1206,54 +1202,6 @@ pub fn clock_epoch() -> Option { check(syscall(SYS_CLOCK_EPOCH, 0, 0, 0, 0)).ok() } -/// Two `u32`s in one argument word. -/// -/// The four device calls below take the claim handle that authorizes them, and -/// `SYS_GPU_PRESENT`'s rectangle then does not fit in what is left. A pair is a -/// wire encoding decoded at the kernel boundary and carried no further. -const fn pair(hi: u32, lo: u32) -> u64 { - ((hi as u64) << 32) | lo as u64 -} - -/// Transfer a region of the framebuffer to the GPU and flush it, presenting the -/// framebuffer claim. Pass (0, 0, 0, 0) to flush the full screen. -/// -/// Fallible: the kernel refuses a handle that is not a live framebuffer claim. -pub fn gpu_present(claim: RawHandle, x: u32, y: u32, w: u32, h: u32) -> Result<(), SyscallError> { - check_unit(syscall(SYS_GPU_PRESENT, claim.0 as u64, pair(x, y), pair(w, h), 0)) -} - -/// Upload the cursor image from backing and enable hardware cursor. -pub fn gpu_set_cursor(claim: RawHandle, hot_x: u32, hot_y: u32) -> Result<(), SyscallError> { - check_unit(syscall(SYS_GPU_SET_CURSOR, claim.0 as u64, hot_x as u64, hot_y as u64, 0)) -} - -/// Move the hardware cursor to screen position (x, y). -pub fn gpu_move_cursor(claim: RawHandle, x: u32, y: u32) -> Result<(), SyscallError> { - check_unit(syscall(SYS_GPU_MOVE_CURSOR, claim.0 as u64, x as u64, y as u64, 0)) -} - -/// Request a GPU resolution change. On success, writes the new -/// [`FramebufferInfo`](crate::FramebufferInfo) to `info_out`. -/// -/// # Safety -/// `info_out` must point to a writable buffer of at least -/// `size_of::()` bytes. -pub unsafe fn gpu_set_resolution( - claim: RawHandle, - width: u32, - height: u32, - info_out: *mut u8, -) -> Result<(), SyscallError> { - check_unit(syscall( - SYS_GPU_SET_RESOLUTION, - claim.0 as u64, - pair(width, height), - info_out as u64, - 0, - )) -} - /// Power the machine off, presenting a `SysCap` that carries /// [`Rights::POWER`](crate::handle::Rights::POWER). /// @@ -2478,10 +2426,9 @@ pub fn device_dma_alloc(claim: RawHandle, bytes: u64) -> Result Result { let mut mapping = DmaMapping { device_addr: 0, bytes: 0 }; check_unit(syscall( diff --git a/sdk/toyos/src/device.rs b/sdk/toyos/src/device.rs index 2f5fad9e98e..901a62a6fa6 100644 --- a/sdk/toyos/src/device.rs +++ b/sdk/toyos/src/device.rs @@ -32,9 +32,7 @@ device_info!( /// **They are installed once.** The kernel remembers what it minted for a /// claim and answers a later read with the same numbers, so the description is /// read once per claim and the handles in it are owned from then on — reading -/// twice and adopting both answers closes one buffer twice. A mode set is the -/// exception and says so: [`FramebufferDev::set_resolution`] remints, and its -/// answer is the fresh set. +/// twice and adopting both answers closes one buffer twice. /// /// Bounded by [`IpcPayload`] and not by `Copy`: the answer's bytes are /// *reinterpreted* as a `T`, so what the bound has to promise is that every bit diff --git a/sdk/toyos/src/gpu.rs b/sdk/toyos/src/gpu.rs deleted file mode 100644 index dae8bf95691..00000000000 --- a/sdk/toyos/src/gpu.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Screen operations, on the framebuffer claim that authorizes them. -//! -//! These were four free functions and the kernel decided whether the caller was -//! the display's owner by comparing pids. A pid is not authority: the claim is, -//! so it is an argument — and a program with no framebuffer claim cannot write -//! the call at all. - -use toyos_abi::FramebufferInfo; -use toyos_abi::syscall::{self, SyscallError}; - -use crate::device::FramebufferDev; -use crate::AsHandle; - -impl FramebufferDev { - /// Transfer a region of the scanout to the GPU and flush it. `(0, 0, 0, 0)` - /// is the whole screen. - pub fn present(&self, x: u32, y: u32, w: u32, h: u32) -> Result<(), SyscallError> { - syscall::gpu_present(self.as_handle(), x, y, w, h) - } - - /// Upload the cursor image from the cursor buffer and enable the hardware - /// cursor. - pub fn set_cursor(&self, hot_x: u32, hot_y: u32) -> Result<(), SyscallError> { - syscall::gpu_set_cursor(self.as_handle(), hot_x, hot_y) - } - - pub fn move_cursor(&self, x: u32, y: u32) -> Result<(), SyscallError> { - syscall::gpu_move_cursor(self.as_handle(), x, y) - } - - /// Ask for a mode change. The answer describes the new scanout, whose - /// buffers are fresh handles — the old ones stay mapped and valid until - /// their holder closes them, because a capability system may not take a - /// mapping away. - pub fn set_resolution(&self, width: u32, height: u32) -> Result { - let mut info = unsafe { core::mem::zeroed::() }; - // SAFETY: `info` is this frame's own storage and outlives the call. - unsafe { - syscall::gpu_set_resolution( - self.as_handle(), - width, - height, - &mut info as *mut FramebufferInfo as *mut u8, - )?; - } - Ok(info) - } -} diff --git a/sdk/toyos/src/lib.rs b/sdk/toyos/src/lib.rs index 1a72dfcdb64..64ade68e0ce 100644 --- a/sdk/toyos/src/lib.rs +++ b/sdk/toyos/src/lib.rs @@ -16,7 +16,6 @@ pub mod census; pub mod device; pub mod endow; pub mod fs; -pub mod gpu; pub mod poller; pub mod power; pub mod wake; diff --git a/system/compositor/Cargo.toml b/system/compositor/Cargo.toml index d5749ada6d5..2e4a6214fab 100644 --- a/system/compositor/Cargo.toml +++ b/system/compositor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "compositor" -description = "The desktop's display server: it claims the framebuffer, keyboard and mouse, composes its clients' windows and hands finished rectangles to the panel." +description = "The desktop's display server: it drives the display, keyboard and mouse it claims, composes its clients' windows and hands finished rectangles to the panel." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" @@ -15,7 +15,10 @@ toyos-input = { path = "../../lib/input" } toyos-inspect = { path = "../../lib/inspect" } toyos-font = { path = "../../sdk/font" } toyos-manifest = { path = "../../lib/manifest" } +toyos-pci-claim = { path = "../../lib/pci-claim" } +toyos-virtio = { path = "../../lib/virtio" } +toyos-virtio-gpu = { path = "virtio-gpu" } toyos-window = { path = "../../sdk/window" } [package.metadata.toyos.host] -exempt.owns = "ToyOS's framebuffer, keyboard and mouse, claimed from the kernel" +exempt.owns = "ToyOS's display, keyboard and mouse: a virtio-gpu function or the framebuffer, claimed from the kernel" diff --git a/system/compositor/desktop/src/lib.rs b/system/compositor/desktop/src/lib.rs index a33652c5706..81e5bcb99c2 100644 --- a/system/compositor/desktop/src/lib.rs +++ b/system/compositor/desktop/src/lib.rs @@ -28,6 +28,7 @@ pub mod damage; pub mod hit; pub mod input; pub mod layout; +pub mod mode; pub mod plan; pub mod rect; pub mod scale; @@ -44,6 +45,7 @@ pub use input::{ Held, KeyAction, MouseSample, PromptGate, Released, TabAction, DRAG_THRESHOLD, }; pub use layout::{set_mode, Chrome, Desk}; +pub use mode::Mode; pub use plan::{compose, content_blit, Blit, Layer}; pub use rect::{Point, Rect}; pub use scale::Scale; diff --git a/system/compositor/desktop/src/mode.rs b/system/compositor/desktop/src/mode.rs new file mode 100644 index 00000000000..72dc3d65b9c --- /dev/null +++ b/system/compositor/desktop/src/mode.rs @@ -0,0 +1,176 @@ +//! The display's mode: the bounds every mode the desktop takes is held to, +//! and what a device's report of its preferred size becomes. + +use core::fmt; + +use toyos_abi::pci::LEND_WINDOW; +use toyos_abi::syscall::SHM_PAGE; + +use crate::layout::Chrome; + +/// A display mode, in pixels. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Mode { + pub width: u32, + pub height: u32, +} + +impl Mode { + pub const fn new(width: u32, height: u32) -> Self { + Self { width, height } + } + + /// One 32-bit scanout of this mode, as the region it is lent in. + pub const fn scanout_bytes(self) -> u64 { + let page = SHM_PAGE as u64; + (self.width as u64 * self.height as u64 * 4).div_ceil(page) * page + } + + fn holds(self, other: Self) -> bool { + self.width >= other.width && self.height >= other.height + } +} + +impl fmt::Display for Mode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}x{}", self.width, self.height) + } +} + +/// A terminal's grid and its cell, which the least mode must hold. +const TERMINAL: (i32, i32) = (80, 24); +const CELL: (i32, i32) = (8, 16); + +/// The least mode: an 80x24 terminal of 8x16 cells in a window frame, above +/// the taskbar, at scale 1. +pub const LEAST: Mode = { + let c = Chrome::DEFAULT; + let w = TERMINAL.0 * CELL.0 + c.chrome_w(); + let h = TERMINAL.1 * CELL.1 + c.chrome_h() + c.taskbar; + Mode::new(w as u32, h as u32) +}; + +/// The largest mode: the largest Intel publishes for the T14's display +/// engine (Intel ARK, Tiger Lake GT2 `8086:9A49`: DisplayPort 7680x4320). +pub const LARGEST: Mode = Mode::new(7680, 4320); + +// A mode change lends the new scanout before it gives the old one back. +const _: () = assert!(2 * LARGEST.scanout_bytes() <= LEND_WINDOW); + +/// The mode a display starts in, from the size its device prefers. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Start { + /// The preferred size, inside the bounds. + Preferred(Mode), + /// The preferred size, clamped axis by axis into them. + Clamped { preferred: Mode, mode: Mode }, + /// No enabled output with a size: the least mode. + Least, +} + +impl Start { + /// `preferred` is the device's: `None` for an output it reports disabled. + pub fn of(preferred: Option) -> Self { + let Some(preferred) = preferred.filter(|m| m.width > 0 && m.height > 0) else { + return Self::Least; + }; + let mode = Mode::new( + preferred.width.clamp(LEAST.width, LARGEST.width), + preferred.height.clamp(LEAST.height, LARGEST.height), + ); + if mode == preferred { + Self::Preferred(mode) + } else { + Self::Clamped { preferred, mode } + } + } + + pub fn mode(self) -> Mode { + match self { + Self::Preferred(mode) | Self::Clamped { mode, .. } => mode, + Self::Least => LEAST, + } + } +} + +/// Why a mode asked for after start is not taken. The current mode stays. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Refused { + BelowLeast(Mode), + PastLargest(Mode), +} + +impl fmt::Display for Refused { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BelowLeast(mode) => write!(f, "{mode} is below the least mode, {LEAST}"), + Self::PastLargest(mode) => write!(f, "{mode} is past the largest mode, {LARGEST}"), + } + } +} + +/// A mode asked for after start, by the device or by a person: taken whole, +/// or refused. +pub fn follow(wanted: Mode) -> Result { + if !wanted.holds(LEAST) { + Err(Refused::BelowLeast(wanted)) + } else if !LARGEST.holds(wanted) { + Err(Refused::PastLargest(wanted)) + } else { + Ok(wanted) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_least_mode_is_a_terminal_over_the_taskbar() { + assert_eq!(LEAST, Mode::new(642, 446)); + } + + /// Two scanouts at the largest mode fill the lend window exactly. + #[test] + fn two_of_the_largest_scanouts_fill_the_lend_window() { + assert_eq!(LARGEST.scanout_bytes(), 64 * SHM_PAGE as u64); + assert_eq!(2 * LARGEST.scanout_bytes(), LEND_WINDOW); + } + + #[test] + fn a_preferred_size_inside_the_bounds_is_the_mode() { + assert_eq!(Start::of(Some(Mode::new(1280, 800))), Start::Preferred(Mode::new(1280, 800))); + assert_eq!(Start::of(Some(LEAST)).mode(), LEAST); + assert_eq!(Start::of(Some(LARGEST)).mode(), LARGEST); + } + + /// A 1x host's zoomed window that already reported 640x360 boots at + /// 642x446, and one past the largest at the largest, axis by axis. + #[test] + fn a_preferred_size_outside_the_bounds_is_clamped_axis_by_axis() { + let preferred = Mode::new(640, 360); + assert_eq!(Start::of(Some(preferred)), Start::Clamped { preferred, mode: Mode::new(642, 446) }); + let preferred = Mode::new(8000, 1000); + assert_eq!(Start::of(Some(preferred)), Start::Clamped { preferred, mode: Mode::new(7680, 1000) }); + } + + #[test] + fn no_output_or_no_size_starts_at_the_least_mode() { + assert_eq!(Start::of(None), Start::Least); + assert_eq!(Start::of(Some(Mode::new(0, 768))), Start::Least); + assert_eq!(Start::Least.mode(), LEAST); + } + + #[test] + fn a_later_mode_is_taken_whole_or_refused() { + assert_eq!(follow(Mode::new(1600, 900)), Ok(Mode::new(1600, 900))); + assert_eq!(follow(LEAST), Ok(LEAST)); + assert_eq!(follow(LARGEST), Ok(LARGEST)); + for small in [Mode::new(641, 900), Mode::new(1600, 445), Mode::new(0, 0)] { + assert_eq!(follow(small), Err(Refused::BelowLeast(small))); + } + for big in [Mode::new(7681, 900), Mode::new(1600, 4321)] { + assert_eq!(follow(big), Err(Refused::PastLargest(big))); + } + } +} diff --git a/system/compositor/desktop/src/scale.rs b/system/compositor/desktop/src/scale.rs index f239efee2e2..b80bfa84d09 100644 --- a/system/compositor/desktop/src/scale.rs +++ b/system/compositor/desktop/src/scale.rs @@ -9,7 +9,7 @@ use core::fmt; -use crate::layout::Chrome; +use crate::mode::LEAST; /// A scale in 120ths: a quarter step from 1 to 4. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -25,19 +25,6 @@ const TARGET_DPI: (u64, u64) = (96 * 28, 24); /// Millimetres per inch, as a ratio. const MM_PER_INCH: (u64, u64) = (254, 10); -/// A terminal's grid and its cell, which the least screen must hold. -const TERMINAL: (i32, i32) = (80, 24); -const CELL: (i32, i32) = (8, 16); - -/// The least logical screen: an 80x24 terminal of 8x16 cells in a window -/// frame, above the taskbar. -const LEAST: (u32, u32) = { - let c = Chrome::DEFAULT; - let w = TERMINAL.0 * CELL.0 + c.chrome_w(); - let h = TERMINAL.1 * CELL.1 + c.chrome_h() + c.taskbar; - (w as u32, h as u32) -}; - impl Scale { pub const ONE: Self = Self(120); @@ -71,8 +58,8 @@ impl Scale { /// Whether `mode` at this scale is at least [`LEAST`] in logical pixels. fn holds_least(self, mode: (u32, u32)) -> bool { let n = u64::from(self.0); - u64::from(mode.0) * 120 >= u64::from(LEAST.0) * n - && u64::from(mode.1) * 120 >= u64::from(LEAST.1) * n + u64::from(mode.0) * 120 >= u64::from(LEAST.width) * n + && u64::from(mode.1) * 120 >= u64::from(LEAST.height) * n } } @@ -99,11 +86,6 @@ mod tests { Scale::auto((w, h), (wmm, hmm)).to_string() } - #[test] - fn the_least_screen_is_a_terminal_over_the_taskbar() { - assert_eq!(LEAST, (642, 446)); - } - /// The panels M1 read: 309x173 mm and 309x174 mm at 1920x1080. #[test] fn the_benches_panels_are_one_and_a_half() { diff --git a/system/compositor/src/display.rs b/system/compositor/src/display.rs new file mode 100644 index 00000000000..7335a0ea17c --- /dev/null +++ b/system/compositor/src/display.rs @@ -0,0 +1,396 @@ +//! The panel the desktop is drawn on: a virtio-gpu function this process +//! drives through its claim, or the firmware's framebuffer the kernel hands +//! over. Virtio is taken first, as soundserver takes its virtio function +//! first: a machine with both shows the desktop on the device that can follow +//! its host's window. +//! +//! The driver's decisions are `toyos-virtio-gpu`'s and host-tested there; +//! this module is the claim under it — the register window, the grant, the +//! interrupt record and the regions lent as scanouts — and what becomes of a +//! refusal. **A refusal after bring-up ends the compositor** by the driver's +//! own word: the desktop cannot outlive its display. + +use std::rc::Rc; +use std::time::Duration; + +use toyos::poller::{Poller, READABLE}; +use toyos::shm::SharedMemory; +use toyos::{endow, AsHandle, DmaRegion, FramebufferDev, PciDev}; +use toyos_abi::syscall::{DeviceType, PciId, SyscallError}; +use toyos_abi::RawHandle; +use toyos_desktop::mode::{self, Start}; +use toyos_desktop::{Mode, Rect, Scale}; +use toyos_pci_claim::virtio::{negotiate, Negotiated}; +use toyos_pci_claim::{Bar, Grant}; +use toyos_virtio::pci::Live; +use toyos_virtio::queue::Published; +use toyos_virtio_gpu::wire::{self, CURSOR_SIDE}; +use toyos_virtio_gpu::{Claim, Declined, Gpu, Next, Queues}; +use window::Screen; + +/// virtio's vendor id and the GPU device's (§4.1.2.1: `0x1040` + 16), as the +/// manifest row spells the claim. +const PCI_ID: PciId = PciId { vendor: 0x1af4, device: 0x1050 }; + +/// The one MSI-X table entry the kernel programs: both queues and the +/// configuration's changes raise it. +const MSIX_ENTRY: u16 = 0; + +/// How long the device may take to answer one wait's worth of commands +/// before the compositor ends. Policy, not physics: QEMU's 2D commands answer +/// in one round trip, and this is a hang's ceiling. +const DEVICE_ANSWER: Duration = Duration::from_secs(10); + +/// What `Screen` takes for a scanout whose bytes are blue, green, red and an +/// unused fourth (`VIRTIO_GPU_FORMAT_B8G8R8X8_UNORM`). +const BGRX: u32 = 1; + +pub enum Display { + Virtio(Virtio), + Gop(Gop), +} + +impl Display { + /// The display this machine gives the compositor; ends it where there is + /// none, since a desktop with no panel has nothing to draw on. + pub fn claim() -> (Self, Scale) { + if let Some(dev) = endow::pci_function::(PCI_ID) { + match Virtio::open(dev) { + Ok(virtio) => { + let scale = virtio.scale; + return (Self::Virtio(virtio), scale); + } + Err(why) => eprintln!("compositor: the virtio-gpu function cannot show the desktop: {why}"), + } + } + let claim: FramebufferDev = endow::device(DeviceType::Framebuffer) + .expect("compositor: this machine gives it neither a virtio-gpu function nor a framebuffer"); + let gop = Gop::open(claim); + let scale = panel_scale(gop.mode, gop.edid()); + (Self::Gop(gop), scale) + } + + pub fn mode(&self) -> Mode { + match self { + Self::Virtio(v) => v.mode, + Self::Gop(g) => g.mode, + } + } + + /// Where a composed frame is blitted. + pub fn screen(&self) -> &Screen { + match self { + Self::Virtio(v) => &v.screen, + Self::Gop(g) => &g.screen, + } + } + + pub fn hw_cursor(&self) -> bool { + matches!(self, Self::Virtio(_)) + } + + /// The claim whose readiness is a wake of the device. + pub fn handle(&self) -> Option { + match self { + Self::Virtio(v) => Some(v.dev.as_handle()), + Self::Gop(_) => None, + } + } + + /// Carry `regions`, already blitted, to the panel. The firmware's + /// framebuffer is the panel: nothing to do. + pub fn present(&mut self, regions: &[Rect]) { + if let Self::Virtio(v) = self { + let rects: Vec = regions + .iter() + .map(|r| wire::Rect { x: r.x0 as u32, y: r.y0 as u32, width: r.w() as u32, height: r.h() as u32 }) + .collect(); + v.gpu.present(&rects).unwrap_or_else(|why| v.fault(why)); + } + } + + /// Show `image`, 64x64 RGBA, as the hardware cursor at `at`. + pub fn set_cursor(&mut self, image: &[u32; (CURSOR_SIDE * CURSOR_SIDE) as usize], at: (u32, u32)) { + if let Self::Virtio(v) = self { + v.gpu.cursor(image, (0, 0), at).unwrap_or_else(|why| v.fault(why)); + } + } + + pub fn move_cursor(&mut self, at: (u32, u32)) { + if let Self::Virtio(v) = self { + v.gpu.move_cursor(at).unwrap_or_else(|why| v.fault(why)); + } + } + + /// The claim read ready: take its record, and do what the wake owes. + pub fn woke(&mut self) { + if let Self::Virtio(v) = self { + match v.dev.irq() { + Ok(_) | Err(SyscallError::WouldBlock) => {} + Err(why) => panic!("compositor: the virtio-gpu claim's interrupt record: {why:?}"), + } + v.gpu.wake().unwrap_or_else(|why| v.fault(why)); + } + } + + /// The mode the device's output now asks for, where a wake noted a display + /// event: the output read again, and its size and scale said. `None` for + /// no event, a disabled output, the mode on screen, or one refused. + pub fn followed(&mut self) -> Option<(Mode, Scale)> { + let Self::Virtio(v) = self else { return None }; + if !v.gpu.display_changed() { + return None; + } + let output = v.gpu.output().unwrap_or_else(|why| v.fault(why)); + let Some((width, height)) = output.preferred else { + println!("compositor: the display event leaves scanout 0 disabled; the mode stays {}", v.mode); + return None; + }; + let wanted = Mode::new(width, height); + let scale = panel_scale(wanted, output.edid.as_ref().map_or(&[][..], |b| &b[..])); + match mode::follow(wanted) { + Ok(mode) if mode != v.mode => Some((mode, scale)), + Ok(_) => None, + Err(why) => { + println!("compositor: the display asks for a mode refused: {why}"); + None + } + } + } + + /// Make a scanout of `mode` for the next frame to be drawn into; the mode + /// on screen stays until [`Self::show`]. + pub fn prepare(&mut self, mode: Mode) -> Result { + let Self::Virtio(v) = self else { + return Err("the firmware's framebuffer has its one mode".to_string()); + }; + match v.gpu.prepare((mode.width, mode.height)).unwrap_or_else(|why| v.fault(why)) { + Ok(next) => { + let screen = scanout_screen(next.backing(), mode); + Ok(Pending { next, screen, mode }) + } + Err(why) => Err(declined(why)), + } + } + + /// Put `pending` on screen, its frame already drawn. + pub fn show(&mut self, pending: Pending) -> Result<(), String> { + let Self::Virtio(v) = self else { unreachable!("only virtio prepares a mode") }; + let Pending { next, screen, mode } = pending; + match v.gpu.show(next).unwrap_or_else(|why| v.fault(why)) { + Ok(()) => { + v.screen = screen; + v.mode = mode; + Ok(()) + } + Err(why) => Err(declined(why)), + } + } +} + +/// A scanout made and not yet shown, and the screen its frame is drawn on. +pub struct Pending { + next: Next, + pub screen: Screen, + mode: Mode, +} + +fn declined(why: Declined) -> String { + match why { + Declined::Lend(why) => format!("the kernel lent no scanout: {why:?}"), + Declined::Device(command) => format!("the device answered {} out of memory", command.name()), + } +} + +/// The scale for `mode` from the panel's size in `edid`, a base block or +/// nothing; 1 where there is no size, each said. +fn panel_scale(mode: Mode, edid: &[u8]) -> Scale { + let said = format!("compositor: display {mode}"); + if edid.is_empty() { + println!("{said}, no panel size, scale 1"); + return Scale::ONE; + } + match toyos_edid::BaseBlock::parse(edid).map(|block| block.image_size()) { + Ok(toyos_edid::ImageSize::Millimetres { width, height }) => { + let scale = Scale::auto((mode.width, mode.height), (width, height)); + println!("{said}, panel {width}x{height} mm, scale {scale}"); + scale + } + Ok(size) => { + println!("{said}, the panel's EDID gives no size ({size:?}), scale 1"); + Scale::ONE + } + Err(why) => { + eprintln!("{said}, the panel's EDID is refused: {why}; scale 1"); + Scale::ONE + } + } +} + +/// The firmware's framebuffer. +pub struct Gop { + /// Held: closing it gives the framebuffer back to the kernel. + _claim: FramebufferDev, + info: toyos_abi::FramebufferInfo, + /// Held because `screen` points into it. + _scanout: SharedMemory, + screen: Screen, + mode: Mode, +} + +impl Gop { + fn open(claim: FramebufferDev) -> Self { + let info = claim.info().expect("compositor: the framebuffer claim's description"); + // A handle the claim's own read installed, so a refusal is the kernel + // contradicting itself and not a client doing anything. + let scanout = SharedMemory::adopt(info.scanout, info.stride as usize * info.height as usize * 4) + .expect("the scanout the framebuffer claim just handed over"); + let screen = Screen::new( + scanout.as_ptr(), + info.width as usize, + info.height as usize, + info.stride as usize, + info.pixel_format, + ); + let mode = Mode::new(info.width, info.height); + Self { _claim: claim, info, _scanout: scanout, screen, mode } + } + + fn edid(&self) -> &[u8] { + self.info + .edid + .get(..self.info.edid_len as usize) + .expect("the kernel hands over an EDID length of 0 or one block") + } +} + +/// A virtio-gpu function, driven. +pub struct Virtio { + gpu: Gpu, + dev: Rc, + screen: Screen, + mode: Mode, + scale: Scale, + /// Held for their mappings' lives: the register window points into the + /// first and the grant into the second. + _bar: SharedMemory, + _region: DmaRegion, +} + +impl Virtio { + /// Bring the function up in the order `toyos-virtio`'s types fix (§3.1.1), + /// read its output (§5.7.5) and show its first mode. + fn open(dev: PciDev) -> Result { + let dev = Rc::new(dev); + let Negotiated { setup, mapping, features } = + negotiate(&dev, wire::F_EDID).map_err(|why| why.to_string())?; + println!("compositor: {features}"); + let edid = setup.features() & wire::F_EDID != 0; + + let (grant, region) = + Grant::alloc(&dev, toyos_virtio_gpu::GRANT_BYTES as u64).map_err(|why| why.to_string())?; + grant.window().zero(); + let queues = Queues::new(grant); + // `num_scanouts` read, so the structure is known to hold the two + // event fields before it. + let (setup, scanouts) = setup + .config_vector(MSIX_ENTRY) + .and_then(|s| s.enable(&queues.control, MSIX_ENTRY)) + .and_then(|s| s.enable(&queues.cursor, MSIX_ENTRY)) + .and_then(|s| s.device_read32(wire::NUM_SCANOUTS)) + .map_err(|why| why.to_string())?; + println!("compositor: virtio-gpu with {scanouts} scanout(s), EDID {}", if edid { "offered" } else { "not offered" }); + + let claim = VirtioClaim { dev: Rc::clone(&dev), live: setup.driver_ok(), poller: Poller::new(1) }; + let mut gpu = Gpu::open(queues, grant, claim, edid); + let output = gpu.output().map_err(|why| why.to_string())?; + if let Some(block) = output.edid { + let hex: String = block.iter().map(|b| format!("{b:02x}")).collect(); + println!("compositor: the device's EDID base block {hex}"); + } + let start = Start::of(output.preferred.map(|(w, h)| Mode::new(w, h))); + match start { + Start::Preferred(_) => {} + Start::Clamped { preferred, mode } => { + println!("compositor: the device prefers {preferred}, clamped to {mode}") + } + Start::Least => println!("compositor: the device prefers no size, so the least mode"), + } + let mode = start.mode(); + let scale = panel_scale(mode, output.edid.as_ref().map_or(&[][..], |b| &b[..])); + let next = match gpu.prepare((mode.width, mode.height)).map_err(|why| why.to_string())? { + Ok(next) => next, + Err(why) => return Err(format!("its first mode, {mode}: {}", declined(why))), + }; + let screen = scanout_screen(next.backing(), mode); + // The first frame is the back buffer's first present; until then the + // scanout is the zeroes the kernel lent it as. + if let Err(why) = gpu.show(next).map_err(|why| why.to_string())? { + return Err(format!("its first mode, {mode}: {}", declined(why))); + } + Ok(Self { gpu, dev, screen, mode, scale, _bar: mapping, _region: region }) + } + + fn fault(&self, why: toyos_virtio_gpu::Refusal) -> ! { + panic!("compositor: the virtio-gpu device cannot be driven on — {why}") + } +} + +fn scanout_screen(lent: &Lent, mode: Mode) -> Screen { + let width = mode.width as usize; + Screen::new(lent.memory.as_ptr(), width, mode.height as usize, width, BGRX) +} + +/// A region lent to the function as a scanout's backing. +pub struct Lent { + memory: SharedMemory, + device_addr: u64, +} + +/// What the driver runs on: the live transport, the claim's interrupt record +/// and the kernel's lending. +struct VirtioClaim { + dev: Rc, + live: Live, + /// Watches the claim alone, for a wait inside one command. + poller: Poller, +} + +impl Claim for VirtioClaim { + type Backing = Lent; + type Refused = SyscallError; + + fn notify(&self, published: Published) { + self.live.notify(published); + } + + fn events_read(&mut self) -> Result { + self.live.device_read32(wire::EVENTS_READ) + } + + fn events_clear(&mut self, events: u32) -> Result<(), toyos_virtio::pci::Refusal> { + self.live.device_write32(wire::EVENTS_CLEAR, events) + } + + fn wait(&mut self) -> bool { + self.poller.watch(&*self.dev, READABLE, 0); + self.poller.wait(1, DEVICE_ANSWER.as_nanos() as u64, |_| {}); + match self.dev.irq() { + Ok(_) => true, + Err(SyscallError::WouldBlock) => false, + Err(why) => panic!("compositor: the virtio-gpu claim's interrupt record: {why:?}"), + } + } + + fn lend(&mut self, bytes: u64) -> Result<(Lent, u64), SyscallError> { + let memory = SharedMemory::create(bytes as usize)?; + let device_addr = self.dev.dma_map(memory.as_handle())?.device_addr; + Ok((Lent { memory, device_addr }, device_addr)) + } + + fn unlend(&mut self, lent: Lent) { + self.dev + .dma_unmap(lent.device_addr) + .unwrap_or_else(|why| panic!("compositor: the kernel would not take back a scanout it lent: {why:?}")); + } +} diff --git a/system/compositor/src/main.rs b/system/compositor/src/main.rs index 9d386ead76b..20b25b76dd5 100644 --- a/system/compositor/src/main.rs +++ b/system/compositor/src/main.rs @@ -17,6 +17,7 @@ #![forbid(unsafe_code)] mod client; +mod display; mod render; mod session; mod stats; @@ -38,11 +39,9 @@ pub const FRAME_INTERVAL: Duration = Duration::from_nanos(16_666_667); // ~60fps /// the clause that makes it "or one frame's worth, whichever is sooner". pub const DRAIN_BUDGET: Duration = FRAME_INTERVAL; -pub const FLAG_HARDWARE_CURSOR: u32 = 1 << 0; - -/// Handles the compositor watches that are not windows: keyboard, mouse, and -/// the two listeners, `compositor` and `prompt`. -pub const FIXED_POLL_HANDLES: u32 = 4; +/// Handles the compositor watches that are not windows: keyboard, mouse, the +/// two listeners, `compositor` and `prompt`, and the display's claim. +pub const FIXED_POLL_HANDLES: u32 = 5; /// The `input` port's listener and each of its sources. pub const INPUT_POLL_HANDLES: u32 = 1 + toyos_input::wire::CONNECTIONS as u32; @@ -52,9 +51,9 @@ pub const INPUT_POLL_HANDLES: u32 = 1 + toyos_input::wire::CONNECTIONS as u32; /// Every window's handle is registered in the same batch as every other /// handle the compositor watches, counted below, and [`Poller::MAX_HANDLES`] /// is the widest set one poller can carry. Unlike the memory budget this does not move when the -/// resolution does, which is why the poller is sized from it: -/// `MSG_SET_RESOLUTION` can make windows cheaper mid-run, and a poller sized -/// for the old screen would then be too small. At any resolution this machine +/// mode does, which is why the poller is sized from it: a mode change can make +/// windows cheaper mid-run, and a poller sized for the old screen would then +/// be too small. At any resolution this machine /// can actually scan out, the memory budget is far below this and is what /// binds. pub const MAX_WINDOW_SLOTS: u32 = Poller::MAX_HANDLES diff --git a/system/compositor/src/render.rs b/system/compositor/src/render.rs index 084daaacc1b..46b21656ead 100644 --- a/system/compositor/src/render.rs +++ b/system/compositor/src/render.rs @@ -353,30 +353,18 @@ pub fn scale_wallpaper( dst } -/// Render the cursor sprite (RGBA) into a 64x64 BGRA hardware cursor buffer. -pub fn upload_cursor( - fb: &toyos::FramebufferDev, - cursor_buf: &mut [u8], - sprite: &sprite::Sprite, - hw_cursor: bool, -) { - let data = sprite.data(); - let w = sprite.width(); - let h = sprite.height(); - cursor_buf[..64 * 64 * 4].fill(0); +/// The cursor sprite, RGBA, as a 64x64 hardware cursor whose pixels' bytes +/// are blue, green, red and alpha. +pub fn cursor_image(sprite: &sprite::Sprite) -> [u32; 64 * 64] { + let (data, w, h) = (sprite.data(), sprite.width(), sprite.height()); + let mut image = [0u32; 64 * 64]; for y in 0..h.min(64) { for x in 0..w.min(64) { - let si = (y * w + x) * 4; - let di = (y * 64 + x) * 4; - cursor_buf[di] = data[si + 2]; - cursor_buf[di + 1] = data[si + 1]; - cursor_buf[di + 2] = data[si]; - cursor_buf[di + 3] = data[si + 3]; + let rgba = &data[(y * w + x) * 4..][..4]; + image[y * 64 + x] = u32::from_le_bytes([rgba[2], rgba[1], rgba[0], rgba[3]]); } } - if hw_cursor { - fb.set_cursor(0, 0).expect("compositor holds the framebuffer claim"); - } + image } /// Draw the cursor sprite into the composed frame (software cursor fallback). diff --git a/system/compositor/src/session.rs b/system/compositor/src/session.rs index 141ba8189ae..46c7192468d 100644 --- a/system/compositor/src/session.rs +++ b/system/compositor/src/session.rs @@ -19,7 +19,7 @@ use toyos::ipc::{Connection, FrameRx, RxStep}; use toyos::poller::{Poller, READABLE}; use toyos::port::Acceptor; use toyos::shm::SharedMemory; -use toyos::{ipc, system, AsHandle, FramebufferDev, Keyboard, Mouse}; +use toyos::{ipc, system, AsHandle, Keyboard, Mouse}; use toyos_abi::input::RawKeyEvent; use toyos_abi::syscall::DeviceType; use toyos_abi::RawHandle; @@ -30,7 +30,7 @@ use toyos_desktop::volume::{overlay_rect, Overlay, Request, Requests, Shown}; use toyos::audio::{Volume, VolumeStep, MSG_VOLUME, MSG_VOLUME_MUTE, MSG_VOLUME_STEP, MSG_VOLUME_UNAVAILABLE}; use toyos_desktop::{ cursor_from_abs, cursor_style, fold_mouse, hit_test, key_action, set_mode, tab_action, Chrome, - CursorStyle, Damage, Desk, Scale, Grab, Held, Hit, KeyAction, Level, Point, Port, PromptGate, Rect, + CursorStyle, Damage, Desk, Scale, Grab, Held, Hit, KeyAction, Level, Mode, Point, Port, PromptGate, Rect, Released, Stack, TabAction, Verdict, Window, WindowId, WindowMode, }; use window::Screen; @@ -40,11 +40,12 @@ use crate::client::{ Client, ClientFrame, ClientRx, CopyRegion, Dead, DropReason, PendingConn, Watcher, Win, HANDSHAKE_TIMEOUT, MAX_KEPT_PAYLOAD, MAX_PENDING_CONNS, }; +use crate::display::Display; use crate::render::{self, Assets, BackBuffer, SystemStats, TitleBarIcons}; use crate::stats::FrameTotals; use crate::{ launcher_apps, CLIPBOARD_POLL_HANDLES, DOUBLE_CLICK_TIME, DRAIN_BUDGET, FIXED_POLL_HANDLES, - FLAG_HARDWARE_CURSOR, FRAME_INTERVAL, INPUT_POLL_HANDLES, MAX_WINDOW_SLOTS, PROMPT_POLL_HANDLES, + FRAME_INTERVAL, INPUT_POLL_HANDLES, MAX_WINDOW_SLOTS, PROMPT_POLL_HANDLES, VOLUME_POLL_HANDLES, }; @@ -100,48 +101,18 @@ struct Wallpaper { } impl Wallpaper { - fn rescale(&mut self, screen: &Screen) { + fn rescale(&mut self, mode: Mode, pixel_format: u32) { self.scaled = render::scale_wallpaper( &self.raw[8..], self.w, self.h, - screen.width(), - screen.height(), - screen.pixel_format_raw() != 0, + mode.width as usize, + mode.height as usize, + pixel_format != 0, ); } } -/// The scale for the mode `info` describes, from the panel's size in the EDID -/// block the claim carries; 1 where it carries none or no size, each said. -fn panel_scale(info: &toyos_abi::FramebufferInfo) -> Scale { - let mode = (info.width, info.height); - let said = format!("compositor: display {}x{}", mode.0, mode.1); - let edid = info - .edid - .get(..info.edid_len as usize) - .expect("the kernel hands over an EDID length of 0 or one block"); - if edid.is_empty() { - println!("{said}, no panel size, scale 1"); - return Scale::ONE; - } - match toyos_edid::BaseBlock::parse(edid).map(|block| block.image_size()) { - Ok(toyos_edid::ImageSize::Millimetres { width, height }) => { - let scale = Scale::auto(mode, (width, height)); - println!("{said}, panel {width}x{height} mm, scale {scale}"); - scale - } - Ok(size) => { - println!("{said}, the panel's EDID gives no size ({size:?}), scale 1"); - Scale::ONE - } - Err(why) => { - eprintln!("{said}, the panel's EDID is refused: {why}; scale 1"); - Scale::ONE - } - } -} - pub struct Session { acceptor: Acceptor, /// The prompt layer's port ([`toyos_manifest::consent::PROMPT`]), which @@ -173,20 +144,10 @@ pub struct Session { pointers: Pointer, poller: Poller, - /// The claim, and the thing every screen call is made *through*: closing it - /// gives the framebuffer back to the kernel, and there is then no handle to - /// present with. - fb_dev: FramebufferDev, - fb_info: toyos_abi::FramebufferInfo, - /// The scale the panel's EDID gives the mode the claim was handed. + display: Display, + /// The scale the panel's EDID gives the mode on screen. scale: Scale, - /// Held because [`Session::screen`] points into it. - _fb_shm: SharedMemory, - screen: Screen, back: BackBuffer, - hw_cursor: bool, - /// The hardware cursor's 64x64 BGRA image. - cursor_shm: SharedMemory, cursors: Cursors, current_cursor: CursorStyle, @@ -249,35 +210,15 @@ impl Session { .expect("the manifest gives this program the mouse"); let inputs = endow::acceptor(wire::PORT) .expect("the manifest declares this program serves `input`"); - let fb_dev: FramebufferDev = endow::device(DeviceType::Framebuffer) - .expect("the manifest gives this program the framebuffer"); - - let fb_info = fb_dev.info().expect("failed to read framebuffer info"); - let scale = panel_scale(&fb_info); - let fb_size = fb_info.stride as usize * fb_info.height as usize * 4; - // The scanout and cursor buffers are handles the claim's own read - // installed, so a refusal here is the kernel contradicting itself and - // not a client doing anything. - let fb_shm = SharedMemory::adopt(fb_info.scanout[0], fb_size) - .expect("the scanout buffer the framebuffer claim just handed over"); - let screen = Screen::new( - fb_shm.as_ptr(), - fb_info.width as usize, - fb_info.height as usize, - fb_info.stride as usize, - fb_info.pixel_format, - ); + let (mut display, scale) = Display::claim(); + let screen = display.screen(); let back = BackBuffer::new(screen.width(), screen.height(), screen.pixel_format_raw()); - let hw_cursor = fb_info.flags & FLAG_HARDWARE_CURSOR != 0; - let mut cursor_shm = SharedMemory::adopt(fb_info.cursor, 64 * 64 * 4) - .expect("the cursor buffer the framebuffer claim just handed over"); let cursors = Cursors { default: Sprite::icon("cursor-bold", [255, 255, 255]), resize: Sprite::icon("arrow-down-right-bold", [255, 255, 255]), crosshair: Sprite::icon("crosshair-simple-bold", [0, 0, 0]), }; - render::upload_cursor(&fb_dev, cursor_shm.as_mut_slice(), &cursors.default, hw_cursor); let font_data = std::fs::read("/system/share/fonts/JetBrainsMono-Regular-8x16.font") .expect("failed to read font"); @@ -290,14 +231,9 @@ impl Session { raw, scaled: Vec::new(), }; - println!( - "compositor: wallpaper {}x{}, scaling to {}x{}", - wallpaper.w, - wallpaper.h, - screen.width(), - screen.height() - ); - wallpaper.rescale(&screen); + let mode = display.mode(); + println!("compositor: wallpaper {}x{}, scaling to {mode}", wallpaper.w, wallpaper.h); + wallpaper.rescale(mode, display.screen().pixel_format_raw()); let icons = TitleBarIcons { minimize: Sprite::icon("minus-bold", [255, 255, 255]), @@ -306,7 +242,7 @@ impl Session { }; let apps = launcher_apps(); - let desk = desk_of(&screen, &font, apps.len()); + let desk = desk_of(mode, &font, apps.len()); let total_mem = total_memory(); let max_windows = toyos_desktop::max_windows(total_mem, desk.screen, MAX_WINDOW_SLOTS as usize); @@ -316,8 +252,8 @@ impl Session { total_mem / (1024 * 1024), ); - // Sized for the slot ceiling rather than for `max_windows`: - // `MSG_SET_RESOLUTION` can raise `max_windows` mid-run. + // Sized for the slot ceiling rather than for `max_windows`: a mode + // change can raise `max_windows` mid-run. let poller = Poller::new( FIXED_POLL_HANDLES + MAX_WINDOW_SLOTS @@ -333,11 +269,13 @@ impl Session { 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); + if let Some(handle) = display.handle() { + poller.watch_raw(handle, READABLE, handle.0 as u64); + } let cursor = Point { x: desk.screen.w() / 2, y: desk.screen.h() / 2 }; - if hw_cursor { - fb_dev.move_cursor(cursor.x as u32, cursor.y as u32) - .expect("compositor holds the framebuffer claim"); + if display.hw_cursor() { + display.set_cursor(&render::cursor_image(&cursors.default), (cursor.x as u32, cursor.y as u32)); } let mut damage = Damage::default(); damage.add(desk.screen); @@ -360,15 +298,10 @@ impl Session { inputs, sources: Default::default(), merge: Merge::default(), - pointers: Pointer::new(fb_info.width, fb_info.height), + pointers: Pointer::new(mode.width, mode.height), poller, - fb_dev, - fb_info, - _fb_shm: fb_shm, - screen, + display, back, - hw_cursor, - cursor_shm, cursors, current_cursor: CursorStyle::Default, font, @@ -430,6 +363,7 @@ impl Session { self.damage.add(overlay_rect(&self.desk)); } self.present(); + self.follow_display(); self.prompt_gone(); } @@ -450,6 +384,7 @@ impl Session { let prompt_ready = self.is_ready(self.prompts.as_handle()); let clipboard_ready = self.is_ready(self.clipboards.as_handle()); let volume_ready = self.volume.as_ref().is_some_and(|(conn, _)| self.is_ready(conn.as_handle())); + let display_ready = self.display.handle().is_some_and(|handle| self.is_ready(handle)); let client_ready = self.stack.iter().any(|w| self.is_ready(w.client.conn.as_handle())) || self.pending.iter().any(|p| self.is_ready(p.conn.as_handle())) || self.watcher.as_ref().is_some_and(|w| self.is_ready(w.conn.as_handle())); @@ -477,11 +412,15 @@ impl Session { && !clipboard_ready && !client_ready && !volume_ready + && !display_ready { return false; } self.dead.clear(); + if display_ready { + self.display.woke(); + } if kb_ready { self.keys(); } @@ -595,7 +534,7 @@ impl Session { } fn pixel_format(&self) -> u32 { - self.screen.pixel_format_raw() + self.display.screen().pixel_format_raw() } /// The kernel's keyboard stream, one source to the merge. @@ -721,9 +660,8 @@ impl Session { let was = self.cursor; self.cursor = cursor_from_abs(sample.abs_x, sample.abs_y, self.desk.screen); - if self.hw_cursor { - self.fb_dev.move_cursor(self.cursor.x as u32, self.cursor.y as u32) - .expect("compositor holds the framebuffer claim"); + if self.display.hw_cursor() { + self.display.move_cursor((self.cursor.x as u32, self.cursor.y as u32)); } else { let (w, h) = self.cursors.extent(); self.damage.add(Rect::new(was.x, was.y, w, h)); @@ -735,12 +673,10 @@ impl Session { cursor_style(&self.desk, &self.stack, &self.grab, self.cursor, self.launcher_open); if wanted != self.current_cursor { self.current_cursor = wanted; - render::upload_cursor( - &self.fb_dev, - self.cursor_shm.as_mut_slice(), - self.cursors.get(wanted), - self.hw_cursor, - ); + if self.display.hw_cursor() { + let image = render::cursor_image(self.cursors.get(wanted)); + self.display.set_cursor(&image, (self.cursor.x as u32, self.cursor.y as u32)); + } } if sample.pressed { @@ -1197,7 +1133,12 @@ impl Session { mark_dead(&mut self.dead, handle, DropReason::OutOfProtocol); continue; }; - self.set_resolution(req.width, req.height); + match toyos_desktop::mode::follow(Mode::new(req.width, req.height)) { + Ok(mode) => { + self.change_mode(mode); + } + Err(why) => println!("compositor: a client asks for a mode refused: {why}"), + } self.answer_resolution(handle); } // The one message a client can ask for faster than it can read @@ -1568,8 +1509,8 @@ impl Session { } fn answer_resolution(&mut self, handle: RawHandle) { - let reply = - window::ResolutionInfo { width: self.fb_info.width, height: self.fb_info.height }; + let mode = self.display.mode(); + let reply = window::ResolutionInfo { width: mode.width, height: mode.height }; if ipc::try_send(handle, window::MSG_RESOLUTION_CHANGED, &reply).is_err() { mark_dead(&mut self.dead, handle, DropReason::NotReading); } @@ -1580,10 +1521,11 @@ impl Session { /// client holding `compositor` can ask. fn answer_inspect(&mut self, handle: RawHandle) { let mut snap = toyos_inspect::Snapshot::new(toyos_inspect::DISPLAY); - snap.put("width", self.fb_info.width); - snap.put("height", self.fb_info.height); + let mode = self.display.mode(); + snap.put("width", mode.width); + snap.put("height", mode.height); snap.put("scale", self.scale.to_string()); - snap.put("cursor", if self.hw_cursor { "hardware" } else { "software" }); + snap.put("cursor", if self.display.hw_cursor() { "hardware" } else { "software" }); snap.put("windows.open", self.stack.len()); snap.put("windows.max", self.max_windows); self.totals.inspect(&mut snap); @@ -1594,32 +1536,19 @@ impl Session { } } - fn set_resolution(&mut self, width: u32, height: u32) { - let Ok(info) = self.fb_dev.set_resolution(width, height) else { - return; - }; - self.fb_info = info; - let size = info.stride as usize * info.height as usize * 4; - self._fb_shm = SharedMemory::adopt(info.scanout[0], size) - .expect("the scanout buffer the mode set just handed over"); - self.screen = Screen::new( - self._fb_shm.as_ptr(), - info.width as usize, - info.height as usize, - info.stride as usize, - info.pixel_format, - ); - self.back = - BackBuffer::new(self.screen.width(), self.screen.height(), self.pixel_format()); - self.desk = desk_of(&self.screen, &self.font, self.apps.len()); - self.pointers.screen(info.width, info.height); + /// Everything sized by the screen, for `mode`: the back buffer, the desk, + /// the pointer's range, the window budget and the wallpaper; every window + /// reflowed onto it and the whole screen damaged. + fn relayout(&mut self, mode: Mode) { + self.back = BackBuffer::new(mode.width as usize, mode.height as usize, self.pixel_format()); + self.desk = desk_of(mode, &self.font, self.apps.len()); + self.pointers.screen(mode.width, mode.height); // What a window costs moved, so what we can afford moved with it. // Windows already open are left alone if the new figure is below their // count — the cap gates creation, it does not evict. self.max_windows = toyos_desktop::max_windows(self.total_mem, self.desk.screen, MAX_WINDOW_SLOTS as usize); - self.wallpaper.rescale(&self.screen); - + self.wallpaper.rescale(mode, self.pixel_format()); for i in 0..self.stack.len() { match self.stack[i].mode { WindowMode::Normal => { @@ -1634,6 +1563,51 @@ impl Session { self.damage.add(self.desk.screen); } + /// Take the mode the display's output now asks for, where it asks. + fn follow_display(&mut self) { + if let Some((mode, scale)) = self.display.followed() { + if self.change_mode(mode) { + self.scale = scale; + } + } + } + + /// Flip to `mode` (§5.7.6.3 on virtio-gpu): its scanout made, the whole + /// desktop laid out and composed into it while the old mode is on screen, + /// then shown. A mode the display declines leaves the old one, said; + /// whether `mode` is on screen. + fn change_mode(&mut self, mode: Mode) -> bool { + let old = self.display.mode(); + if mode == old { + return true; + } + let pending = match self.display.prepare(mode) { + Ok(pending) => pending, + Err(why) => { + println!("compositor: mode {mode} declined, {old} stays: {why}"); + return false; + } + }; + self.relayout(mode); + let regions = self.compose(); + blit(&pending.screen, &self.back, ®ions); + match self.display.show(pending) { + Ok(()) => { + println!("compositor: mode {mode}"); + if self.display.hw_cursor() { + self.display.move_cursor((self.cursor.x as u32, self.cursor.y as u32)); + } + self.frame_callbacks(®ions); + true + } + Err(why) => { + println!("compositor: mode {mode} declined, {old} stays: {why}"); + self.relayout(old); + false + } + } + } + fn reap(&mut self) { announce(&self.dead); if self.dead.is_empty() { @@ -1666,6 +1640,9 @@ 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, clipboards: bool) { + if let Some(handle) = self.display.handle().filter(|handle| self.is_ready(*handle)) { + self.poller.watch_raw(handle, READABLE, handle.0 as u64); + } if let Some((conn, _)) = &self.volume { let handle = conn.as_handle(); if self.is_ready(handle) { @@ -1742,16 +1719,28 @@ impl Session { } fn present(&mut self) { - if self.damage.is_empty() { - return; - } - let regions = self.damage.take(self.desk.screen); - if regions.is_empty() { - return; - } // Two clock syscalls per composited frame — 120/s at the frame cap — // which is what any measure of a frame costs here. let started = Instant::now(); + let regions = self.compose(); + if regions.is_empty() { + return; + } + let damage_px = blit(self.display.screen(), &self.back, ®ions); + self.totals.record(started.elapsed().as_nanos() as u64, regions.len(), damage_px); + self.display.present(®ions); + if self.totals.frames() == 1 { + println!("compositor: the first frame is on the panel"); + } + self.frame_callbacks(®ions); + } + + /// Compose every damaged region into the back buffer, and answer them. + fn compose(&mut self) -> Vec { + if self.damage.is_empty() { + return Vec::new(); + } + let regions = self.damage.take(self.desk.screen); let assets = Assets { font: &self.font, icons: &self.icons, @@ -1780,7 +1769,7 @@ impl Session { // Into the back buffer, so a region containing the cursor carries it // over with everything else rather than the panel being touched twice. - if !self.hw_cursor { + if !self.display.hw_cursor() { let sprite = self.cursors.get(self.current_cursor); let rect = Rect::new( self.cursor.x, @@ -1792,33 +1781,7 @@ impl Session { render::draw_software_cursor(&self.back.surface, sprite, self.cursor); } } - - let mut damage_px = 0; - for region in ®ions { - self.screen.blit( - region.x0 as usize, - region.y0 as usize, - region.w() as usize, - region.h() as usize, - self.back.surface.width(), - self.back.region(*region), - ); - damage_px += region.area(); - } - self.totals.record(started.elapsed().as_nanos() as u64, regions.len(), damage_px); - - for region in ®ions { - self.fb_dev - .present( - region.x0 as u32, - region.y0 as u32, - region.w() as u32, - region.h() as u32, - ) - .expect("compositor holds the framebuffer claim"); - } - - self.frame_callbacks(®ions); + regions } /// Tell every window that presented and was composited that its frame is @@ -1984,10 +1947,27 @@ fn launcher() -> mpsc::Sender { launches } -fn desk_of(screen: &Screen, font: &font::Font, apps: usize) -> Desk { +/// Carry `regions` of the back buffer to `screen`, and answer the pixels. +fn blit(screen: &Screen, back: &BackBuffer, regions: &[Rect]) -> u64 { + let mut pixels = 0; + for region in regions { + screen.blit( + region.x0 as usize, + region.y0 as usize, + region.w() as usize, + region.h() as usize, + back.surface.width(), + back.region(*region), + ); + pixels += region.area(); + } + pixels +} + +fn desk_of(mode: Mode, font: &font::Font, apps: usize) -> Desk { Desk { chrome: Chrome::DEFAULT, - screen: Rect::new(0, 0, screen.width() as i32, screen.height() as i32), + screen: Rect::new(0, 0, mode.width as i32, mode.height as i32), font_w: font.width() as i32, apps, } diff --git a/system/compositor/src/stats.rs b/system/compositor/src/stats.rs index d52b33aa693..c931959071c 100644 --- a/system/compositor/src/stats.rs +++ b/system/compositor/src/stats.rs @@ -9,10 +9,9 @@ pub struct FrameTotals { impl FrameTotals { /// `composite_ns` covers composing every region of the frame, the software - /// cursor and the blits that carry them to the panel — everything between - /// one frame's damage being taken and it being on screen. Not the - /// `gpu::present` calls that follow: those are syscalls, and on the - /// firmware framebuffer they do nothing at all. + /// cursor and the blits that carry them to the scanout. Not the display's + /// present that follows: virtio-gpu's transfers and flushes, and nothing + /// at all on the firmware framebuffer. pub fn record(&mut self, composite_ns: u64, rects: usize, damage_px: u64) { self.frames += 1; self.rects += rects as u64; @@ -20,6 +19,10 @@ impl FrameTotals { self.composite_ns += composite_ns; } + pub fn frames(&self) -> u64 { + self.frames + } + pub fn inspect(&self, snap: &mut toyos_inspect::Snapshot) { snap.put("frames.composited", self.frames); snap.put("frames.rects", self.rects); diff --git a/system/compositor/virtio-gpu/Cargo.toml b/system/compositor/virtio-gpu/Cargo.toml new file mode 100644 index 00000000000..bd3e2fead26 --- /dev/null +++ b/system/compositor/virtio-gpu/Cargo.toml @@ -0,0 +1,24 @@ +# A member of the host workspace (root `Cargo.toml`), like +# `toyos-virtio-sound`: pure, and its tests run on the host against a model of +# the device, with §5.7's structures as the oracle. + +[package] +name = "toyos-virtio-gpu" +description = "The compositor's virtio-gpu driver: the GPU device's 2D commands (VirtIO 1.2 §5.7) over its two queues, the scanout's page flip and the cursor, pure." +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +publish = false + +[lib] +doctest = false + +[dependencies] +# The split virtqueue every word the device writes back is bounded by, and the +# transport's refusals. +toyos-virtio = { path = "../../../lib/virtio" } +# `DmaBuffers`, the grant both queues, their buffers and the cursor are in. +toyos-device-memory = { path = "../../../lib/device-memory" } + +[lints.rust] +warnings = "deny" diff --git a/system/compositor/virtio-gpu/src/lib.rs b/system/compositor/virtio-gpu/src/lib.rs new file mode 100644 index 00000000000..6c4d7578021 --- /dev/null +++ b/system/compositor/virtio-gpu/src/lib.rs @@ -0,0 +1,650 @@ +//! The compositor's virtio-gpu driver: one scanout through the device's two +//! queues, its page flip and its cursor, every decision about them, and none +//! of the instructions that carry them out. +//! +//! Every `§` here is a section of *Virtual I/O Device (VIRTIO) Version 1.2*, +//! OASIS Committee Specification 01. [`wire`] is §5.7.6's messages; this +//! module is the queues they travel on, laid out in one grant, over +//! `toyos-virtio`'s split virtqueue and the [`Claim`] its holder implements. +//! +//! # Every control command is fenced +//! +//! §5.7.6.5 lets the device return a command before processing it unless the +//! command carries `VIRTIO_GPU_FLAG_FENCE`. So every control command carries +//! it and an id of its own, and an answer without the flag or with another id +//! is refused: no completion this driver relies on is assumed. Commands whose +//! order matters are not in flight together — a frame's transfers are +//! answered before its flushes are sent — since §5.7.6.5 promises completion +//! and not order. +//! +//! # The device is not trusted +//! +//! Every word it writes back — a used element, an answer, a configuration +//! field — is input from outside the driver. What `toyos-virtio` bounds it +//! bounds; what is the GPU device's is bounded here, and one that fails is a +//! [`Refusal`] by name, the end of the device's use. One answer is not a +//! fault: `RESP_ERR_OUT_OF_MEMORY` to the three commands that make a scanout +//! is the device declining a mode ([`Declined`]), and the mode on screen +//! stays. +//! +//! # Events +//! +//! One interrupt serves both queues and configuration changes. Every wake of +//! the claim, inside a command's wait too, drains the cursor queue and reads +//! `events_read`, so a display event a wait consumed is noted +//! ([`Gpu::display_changed`]) and never lost. [`Gpu::output`] clears the +//! events before it asks for the output, so one raised during its reads +//! raises another (§5.7.4.2). +//! +//! # What is not here +//! +//! No 3D, no blob resources, one scanout (scanout 0), and one backing entry +//! per resource: a lent scanout is one contiguous run of device addresses. + +#![no_std] +#![forbid(unsafe_code)] + +extern crate alloc; +#[cfg(test)] +extern crate std; + +pub mod wire; + +#[cfg(test)] +mod tests; + +use toyos_device_memory::DmaBuffers; +use toyos_virtio::pci; +use toyos_virtio::queue::{Buffer, Parts, Published, Used, UsedRefusal, Virtqueue}; + +use wire::{Command, Rect, CURSOR_SIDE, FLAG_FENCE, HDR_BYTES}; + +/// Descriptors per queue, powers of two (§2.7). Every chain is two: a +/// request the device reads and an answer it writes. +pub const CONTROL_QUEUE_SIZE: u16 = 16; +pub const CURSOR_QUEUE_SIZE: u16 = 16; +const CHAIN: u16 = 2; +const CONTROL_SLOTS: usize = (CONTROL_QUEUE_SIZE / CHAIN) as usize; +const CURSOR_SLOTS: usize = (CURSOR_QUEUE_SIZE / CHAIN) as usize; + +/// The longest request: `RESOURCE_ATTACH_BACKING` with its one entry. +const REQUEST_BYTES: u32 = HDR_BYTES + 8 + 16; +/// §5.7.6.10's `virtio_gpu_update_cursor`. +const CURSOR_REQUEST_BYTES: u32 = HDR_BYTES + 32; + +/// The grant's layout: each queue on a page of its own, then each control +/// slot's request and answer, each cursor slot's, and the cursor's image. +const CONTROL_PARTS: Parts = Parts::contiguous(0x0000, CONTROL_QUEUE_SIZE); +const CURSOR_PARTS: Parts = Parts::contiguous(0x1000, CURSOR_QUEUE_SIZE); +const CONTROL_SLOT_AT: usize = 0x2000; +const CONTROL_SLOT_BYTES: usize = 0x500; +const CONTROL_ANSWER: usize = 0x40; +const CURSOR_SLOT_AT: usize = CONTROL_SLOT_AT + CONTROL_SLOTS * CONTROL_SLOT_BYTES; +const CURSOR_SLOT_BYTES: usize = 0x80; +const CURSOR_ANSWER: usize = 0x40; +const CURSOR_IMAGE: usize = 0x5000; +const CURSOR_IMAGE_BYTES: u32 = CURSOR_SIDE * CURSOR_SIDE * 4; +pub const GRANT_BYTES: usize = CURSOR_IMAGE + CURSOR_IMAGE_BYTES as usize; + +const _: () = { + assert!(CONTROL_PARTS.end(CONTROL_QUEUE_SIZE) <= CURSOR_PARTS.desc); + assert!(CURSOR_PARTS.end(CURSOR_QUEUE_SIZE) <= CONTROL_SLOT_AT); + assert!(REQUEST_BYTES as usize <= CONTROL_ANSWER); + assert!(CONTROL_ANSWER + wire::EDID_BYTES as usize <= CONTROL_SLOT_BYTES); + assert!(CURSOR_REQUEST_BYTES as usize <= CURSOR_ANSWER); + assert!(CURSOR_ANSWER + HDR_BYTES as usize <= CURSOR_SLOT_BYTES); + assert!(CURSOR_SLOT_AT + CURSOR_SLOTS * CURSOR_SLOT_BYTES <= CURSOR_IMAGE); +}; + +/// The cursor's resource; every scanout's counts up from the next. +const CURSOR_RESOURCE: u32 = 1; +const FIRST_SCANOUT_RESOURCE: u32 = 2; + +/// The one scanout this driver drives. +const SCANOUT: u32 = 0; + +/// What the driver needs of the claim it runs on. +pub trait Claim { + /// A region lent to the function, which the holder draws a scanout into. + type Backing; + /// Why the holder could not lend one. + type Refused: Copy + core::fmt::Debug; + + /// Tell the device of the chain `published` made available. + fn notify(&self, published: Published); + /// §5.7.4's `events_read` and `events_clear`. + fn events_read(&mut self) -> Result; + fn events_clear(&mut self, events: u32) -> Result<(), pci::Refusal>; + /// Block until the claim's interrupt record is readable, and take it; + /// `false` when the holder's bound passed first. + fn wait(&mut self) -> bool; + /// `bytes` of memory the function may read, contiguous at the device + /// address answered beside it. + fn lend(&mut self, bytes: u64) -> Result<(Self::Backing, u64), Self::Refused>; + /// Take back what [`Self::lend`] answered: the device reaches none of it + /// from here. + fn unlend(&mut self, backing: Self::Backing); +} + +/// Why the device is not driven. Each is something it answered, or did not. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Refusal { + /// A used ring `toyos-virtio` did not believe. + Used(u16, UsedRefusal), + /// It never answered: the holder's wait passed its bound. + Silent(Command), + /// It answered with fewer bytes than the answer's structure. + ShortAnswer { command: Command, written: u32, wanted: u32 }, + /// The answer to a fenced command does not carry the fence. + Unfenced { command: Command, flags: u32, fence: u64, sent: u64 }, + /// It answered with an error, or with another command's answer. + Answered { command: Command, answer: u32 }, + /// A cursor command's answer is neither none nor a header (§5.7.6.10). + CursorAnswer { command: Command, written: u32 }, + /// Its configuration, as the transport refused it. + Config(pci::Refusal), +} + +impl core::fmt::Display for Refusal { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Used(queue, why) => write!(f, "its queue {queue}'s used ring: {why}"), + Self::Silent(command) => write!(f, "it never answered {}", command.name()), + Self::ShortAnswer { command, written, wanted } => write!( + f, + "it answered {} with {written} byte(s), where the answer is {wanted}", + command.name() + ), + Self::Unfenced { command, flags, fence, sent } => write!( + f, + "it answered {} with flags {flags:#x} and fence {fence}, where it was sent fence \ + {sent}", + command.name() + ), + Self::Answered { command, answer } => match wire::answer_name(*answer) { + Some(name) => write!(f, "it answered {} with {name}", command.name()), + None => write!(f, "it answered {} with {answer:#x}, which is no answer", command.name()), + }, + Self::CursorAnswer { command, written } => write!( + f, + "it answered {} with {written} byte(s), which is neither none nor a header", + command.name() + ), + Self::Config(why) => write!(f, "its configuration: {why}"), + } + } +} + +impl From for Refusal { + fn from(why: pci::Refusal) -> Self { + Self::Config(why) + } +} + +/// Why a mode was not taken. The mode on screen stays. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Declined { + /// The holder could not lend a scanout. + Lend(E), + /// The device answered `RESP_ERR_OUT_OF_MEMORY` to this command. + Device(Command), +} + +/// A mode's step: done, or declined with the mode on screen kept. +pub type Taken = Result>; + +/// What the device says of its output (§5.7.6.8). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Output { + /// Scanout 0's preferred size, `None` where it is disabled. + pub preferred: Option<(u32, u32)>, + /// Its EDID's base block, where `VIRTIO_GPU_F_EDID` was negotiated. + pub edid: Option<[u8; 128]>, +} + +/// A scanout resource: created, backed, and the region behind it. +struct Scanout { + id: u32, + mode: (u32, u32), + backing: B, +} + +/// A scanout made and not yet shown: [`Gpu::prepare`] answers it, the holder +/// draws its first frame into [`Self::backing`], and [`Gpu::show`] takes it. +pub struct Next(Scanout); + +impl Next { + pub fn backing(&self) -> &B { + &self.0.backing + } + + pub fn mode(&self) -> (u32, u32) { + self.0.mode + } +} + +/// A control command in flight, at its slot. +#[derive(Clone, Copy)] +struct InFlight { + command: Command, + fence: u64, +} + +/// What an answered control command came to. +#[derive(Clone, Copy, PartialEq, Eq)] +enum Outcome { + Done, + OutOfMemory, +} + +/// The two queues, laid out in the grant before the transport is given them: +/// [`toyos_virtio::pci::Setup::enable`] takes each, and [`Gpu::open`] takes +/// them back once the device is live. +pub struct Queues { + pub control: Virtqueue, + pub cursor: Virtqueue, +} + +impl Queues { + /// # Panics + /// If the grant is shorter than [`GRANT_BYTES`]: the caller's own mistake. + pub fn new(mem: M) -> Self { + Self { + control: Virtqueue::new(mem.clone(), wire::CONTROL_QUEUE, CONTROL_QUEUE_SIZE, CONTROL_PARTS), + cursor: Virtqueue::new(mem, wire::CURSOR_QUEUE, CURSOR_QUEUE_SIZE, CURSOR_PARTS), + } + } +} + +/// The device, live. +pub struct Gpu { + mem: M, + claim: C, + queues: Queues, + control: [Option; CONTROL_SLOTS], + cursor: [Option; CURSOR_SLOTS], + next_fence: u64, + next_resource: u32, + edid: bool, + display_changed: bool, + scanout: Option>, + cursor_made: bool, +} + +impl Gpu { + /// The device past `DRIVER_OK`, with `queues` enabled on it. `edid` is + /// whether `VIRTIO_GPU_F_EDID` was negotiated. + pub fn open(queues: Queues, mem: M, claim: C, edid: bool) -> Self { + Self { + mem, + claim, + queues, + control: [None; CONTROL_SLOTS], + cursor: [None; CURSOR_SLOTS], + next_fence: 1, + next_resource: FIRST_SCANOUT_RESOURCE, + edid, + display_changed: false, + scanout: None, + cursor_made: false, + } + } + + pub fn claim(&self) -> &C { + &self.claim + } + + /// The mode on screen, once one is. + pub fn mode(&self) -> Option<(u32, u32)> { + self.scanout.as_ref().map(|s| s.mode) + } + + /// The region the scanout on screen is drawn into. + pub fn backing(&self) -> Option<&C::Backing> { + self.scanout.as_ref().map(|s| &s.backing) + } + + /// What a wake of the claim owes, its record already taken: the cursor + /// queue drained, and `events_read` read for a display event. + pub fn wake(&mut self) -> Result<(), Refusal> { + self.drain_cursor()?; + if self.claim.events_read()? & wire::EVENT_DISPLAY != 0 { + self.display_changed = true; + } + Ok(()) + } + + /// Whether a wake found a display event since the last [`Self::output`]. + pub fn display_changed(&self) -> bool { + self.display_changed + } + + /// The output as the device describes it now (§5.7.5): the events + /// cleared, then `GET_DISPLAY_INFO`, then `GET_EDID` for scanout 0. + pub fn output(&mut self) -> Result { + let events = self.claim.events_read()?; + self.claim.events_clear(events)?; + self.display_changed = false; + + let slot = self.command(Command::GetDisplayInfo, &[])?.0; + // §5.7.6.8: `pmodes[0]` follows the header: its rect, `enabled`, `flags`. + let at = Self::answer_at(slot) + HDR_BYTES as usize; + let (width, height) = (self.mem.read32(at + 8), self.mem.read32(at + 12)); + let enabled = self.mem.read32(at + 16) != 0; + let preferred = enabled.then_some((width, height)); + + let edid = if self.edid { + let slot = self.command(Command::GetEdid, &[SCANOUT, 0])?.0; + let at = Self::answer_at(slot) + wire::EDID_AT; + let mut block = [0u8; 128]; + for (nth, word) in block.as_chunks_mut::<4>().0.iter_mut().enumerate() { + *word = self.mem.read32(at + 4 * nth).to_le_bytes(); + } + Some(block) + } else { + None + }; + Ok(Output { preferred, edid }) + } + + /// Make a scanout of `mode` (§5.7.6.1): lend its backing, create its + /// resource, attach the backing. The mode on screen stays until + /// [`Self::show`]. + /// + /// # Panics + /// If `mode` is empty or its scanout is 4 GiB or more: the caller bounds + /// the modes it asks for. + pub fn prepare(&mut self, mode: (u32, u32)) -> Result, C::Refused>, Refusal> { + let (width, height) = mode; + let bytes = u64::from(width) * u64::from(height) * 4; + let length = u32::try_from(bytes) + .ok() + .filter(|b| *b > 0) + .unwrap_or_else(|| panic!("virtio-gpu: {width}x{height} is no mode this driver makes")); + let (backing, addr) = match self.claim.lend(bytes) { + Ok(lent) => lent, + Err(why) => return Ok(Err(Declined::Lend(why))), + }; + let id = self.next_resource; + self.next_resource = id.checked_add(1).expect("virtio-gpu: resource ids run out after 2^32 modes"); + + let create = [id, wire::FORMAT_B8G8R8X8_UNORM, width, height]; + if self.command(Command::ResourceCreate2d, &create)?.1 == Outcome::OutOfMemory { + self.claim.unlend(backing); + return Ok(Err(Declined::Device(Command::ResourceCreate2d))); + } + let attach = [id, 1, addr as u32, (addr >> 32) as u32, length, 0]; + if self.command(Command::ResourceAttachBacking, &attach)?.1 == Outcome::OutOfMemory { + self.unref(id)?; + self.claim.unlend(backing); + return Ok(Err(Declined::Device(Command::ResourceAttachBacking))); + } + Ok(Ok(Next(Scanout { id, mode, backing }))) + } + + /// Put `next` on screen (§5.7.6.3): its whole frame transferred, + /// `SET_SCANOUT`, a whole flush; then the scanout it replaced unref'd and, + /// only once that is answered, its backing given back. + pub fn show(&mut self, next: Next) -> Result, Refusal> { + let Next(new) = next; + let whole = Rect::whole(new.mode.0, new.mode.1); + self.transfer(new.id, whole, new.mode.0)?; + self.settle()?; + let mut set = [0; 6]; + set[..4].copy_from_slice(&whole.words()); + set[4..].copy_from_slice(&[SCANOUT, new.id]); + if self.command(Command::SetScanout, &set)?.1 == Outcome::OutOfMemory { + self.unref(new.id)?; + self.claim.unlend(new.backing); + return Ok(Err(Declined::Device(Command::SetScanout))); + } + self.flush(new.id, whole)?; + self.settle()?; + if let Some(old) = self.scanout.replace(new) { + self.unref(old.id)?; + self.claim.unlend(old.backing); + } + Ok(Ok(())) + } + + /// Carry `rects` of the scanout on screen to the display (§5.7.6.2): + /// every transfer answered, then every flush. + /// + /// # Panics + /// With no scanout on screen, or a rectangle outside it: the caller's + /// own mistake. + pub fn present(&mut self, rects: &[Rect]) -> Result<(), Refusal> { + let (id, (width, height)) = { + let s = self.scanout.as_ref().expect("virtio-gpu: a present with no scanout on screen"); + (s.id, s.mode) + }; + for r in rects { + assert!( + r.x.checked_add(r.width).is_some_and(|x| x <= width) + && r.y.checked_add(r.height).is_some_and(|y| y <= height), + "virtio-gpu: {r:?} is outside the {width}x{height} scanout" + ); + } + for r in rects { + self.transfer(id, *r, width)?; + } + self.settle()?; + for r in rects { + self.flush(id, *r)?; + } + self.settle() + } + + /// The cursor's image and hotspot, at `at` (§5.7.6.6): its resource made + /// the first time, the image transferred and answered, then + /// `UPDATE_CURSOR`. `image` is 64x64 pixels whose bytes are blue, green, + /// red and alpha. + pub fn cursor(&mut self, image: &[u32; (CURSOR_SIDE * CURSOR_SIDE) as usize], hot: (u32, u32), at: (u32, u32)) -> Result<(), Refusal> { + if !self.cursor_made { + let create = [CURSOR_RESOURCE, wire::FORMAT_B8G8R8A8_UNORM, CURSOR_SIDE, CURSOR_SIDE]; + self.required(Command::ResourceCreate2d, &create)?; + let addr = self.mem.device_addr(CURSOR_IMAGE); + let attach = [CURSOR_RESOURCE, 1, addr as u32, (addr >> 32) as u32, CURSOR_IMAGE_BYTES, 0]; + self.required(Command::ResourceAttachBacking, &attach)?; + self.cursor_made = true; + } + for (nth, pixel) in image.iter().enumerate() { + self.mem.write32(CURSOR_IMAGE + 4 * nth, *pixel); + } + self.transfer(CURSOR_RESOURCE, Rect::whole(CURSOR_SIDE, CURSOR_SIDE), CURSOR_SIDE)?; + self.settle()?; + self.cursor_send(Command::UpdateCursor, at, CURSOR_RESOURCE, hot) + } + + /// Move the cursor to `at` (§5.7.6.10). + pub fn move_cursor(&mut self, at: (u32, u32)) -> Result<(), Refusal> { + self.cursor_send(Command::MoveCursor, at, 0, (0, 0)) + } + + fn answer_at(slot: usize) -> usize { + CONTROL_SLOT_AT + slot * CONTROL_SLOT_BYTES + CONTROL_ANSWER + } + + fn transfer(&mut self, id: u32, r: Rect, width: u32) -> Result<(), Refusal> { + // §5.7.6.8: the offset into the resource's backing of the rect's first + // byte, which this driver lays out row after row of `width` pixels. + let offset = (u64::from(r.y) * u64::from(width) + u64::from(r.x)) * 4; + let mut words = [0; 8]; + words[..4].copy_from_slice(&r.words()); + words[4..].copy_from_slice(&[offset as u32, (offset >> 32) as u32, id, 0]); + self.send(Command::TransferToHost2d, &words).map(drop) + } + + fn flush(&mut self, id: u32, r: Rect) -> Result<(), Refusal> { + let mut words = [0; 6]; + words[..4].copy_from_slice(&r.words()); + words[4..].copy_from_slice(&[id, 0]); + self.send(Command::ResourceFlush, &words).map(drop) + } + + fn unref(&mut self, id: u32) -> Result<(), Refusal> { + self.required(Command::ResourceUnref, &[id, 0]) + } + + /// A command no answer but success may end. + fn required(&mut self, command: Command, payload: &[u32]) -> Result<(), Refusal> { + match self.command(command, payload)?.1 { + Outcome::Done => Ok(()), + Outcome::OutOfMemory => { + Err(Refusal::Answered { command, answer: wire::RESP_ERR_OUT_OF_MEMORY }) + } + } + } + + /// One command alone in flight, sent and answered: its slot, where its + /// answer is, and what it came to. + fn command(&mut self, command: Command, payload: &[u32]) -> Result<(usize, Outcome), Refusal> { + assert!( + self.control.iter().all(Option::is_none), + "virtio-gpu: {} sent with commands still in flight", + command.name() + ); + self.send(command, payload)?; + let (slot, _, outcome) = self.reap()?; + Ok((slot, outcome)) + } + + /// Publish `command` on a free control slot, fenced, waiting for one + /// where every slot is in flight. + fn send(&mut self, command: Command, payload: &[u32]) -> Result { + let slot = loop { + match self.control.iter().position(Option::is_none) { + Some(free) => break free, + None => { + self.reap()?; + } + } + }; + let fence = self.next_fence; + self.next_fence += 1; + let at = CONTROL_SLOT_AT + slot * CONTROL_SLOT_BYTES; + let words = wire::header(command, FLAG_FENCE, fence).into_iter().chain(payload.iter().copied()); + let mut bytes = 0; + for (nth, word) in words.enumerate() { + self.mem.write32(at + 4 * nth, word); + bytes += 4; + } + let chain = [ + Buffer::readable(self.mem.device_addr(at), bytes), + Buffer::writable(self.mem.device_addr(at + CONTROL_ANSWER), command.ok().1), + ]; + let published = self.queues.control.publish(slot as u16 * CHAIN, &chain); + self.claim.notify(published); + self.control[slot] = Some(InFlight { command, fence }); + Ok(slot) + } + + /// Every control command in flight answered, each answer success. + fn settle(&mut self) -> Result<(), Refusal> { + while self.control.iter().any(Option::is_some) { + if let (_, command, Outcome::OutOfMemory) = self.reap()? { + return Err(Refusal::Answered { command, answer: wire::RESP_ERR_OUT_OF_MEMORY }); + } + } + Ok(()) + } + + /// The next control command the device answers, waiting for it, and its + /// answer held to the command: the fence, the length and the type. + fn reap(&mut self) -> Result<(usize, Command, Outcome), Refusal> { + let used = loop { + if let Some(used) = used(&mut self.queues.control)? { + break used; + } + let waiting = self.control.iter().flatten().next().expect("virtio-gpu: a wait with nothing in flight").command; + if !self.claim.wait() { + return Err(Refusal::Silent(waiting)); + } + self.wake()?; + }; + let Used { head, written } = used; + // A head `toyos-virtio` answered is one a chain was published at, and + // this driver publishes one only at a slot's. + let slot = (head / CHAIN) as usize; + let InFlight { command, fence: sent } = + self.control[slot].take().expect("virtio-gpu: toyos-virtio answered a slot with nothing in flight"); + if written < HDR_BYTES { + return Err(Refusal::ShortAnswer { command, written, wanted: HDR_BYTES }); + } + let at = Self::answer_at(slot); + let answer = self.mem.read32(at); + let flags = self.mem.read32(at + 4); + let fence = u64::from(self.mem.read32(at + 8)) | u64::from(self.mem.read32(at + 12)) << 32; + if flags & FLAG_FENCE == 0 || fence != sent { + return Err(Refusal::Unfenced { command, flags, fence, sent }); + } + let (ok, wanted) = command.ok(); + if answer == ok { + if written < wanted { + return Err(Refusal::ShortAnswer { command, written, wanted }); + } + return Ok((slot, command, Outcome::Done)); + } + let makes_a_scanout = matches!( + command, + Command::ResourceCreate2d | Command::ResourceAttachBacking | Command::SetScanout + ); + if answer == wire::RESP_ERR_OUT_OF_MEMORY && makes_a_scanout { + return Ok((slot, command, Outcome::OutOfMemory)); + } + Err(Refusal::Answered { command, answer }) + } + + /// Publish a cursor command, unfenced: §5.7.6.10 has the cursor queue + /// carry no data the driver waits on. + fn cursor_send(&mut self, command: Command, at: (u32, u32), resource: u32, hot: (u32, u32)) -> Result<(), Refusal> { + let slot = loop { + self.drain_cursor()?; + match self.cursor.iter().position(Option::is_none) { + Some(free) => break free, + None if self.claim.wait() => self.wake()?, + None => return Err(Refusal::Silent(command)), + } + }; + let base = CURSOR_SLOT_AT + slot * CURSOR_SLOT_BYTES; + let payload = [SCANOUT, at.0, at.1, 0, resource, hot.0, hot.1, 0]; + for (nth, word) in wire::header(command, 0, 0).into_iter().chain(payload).enumerate() { + self.mem.write32(base + 4 * nth, word); + } + let chain = [ + Buffer::readable(self.mem.device_addr(base), CURSOR_REQUEST_BYTES), + Buffer::writable(self.mem.device_addr(base + CURSOR_ANSWER), HDR_BYTES), + ]; + let published = self.queues.cursor.publish(slot as u16 * CHAIN, &chain); + self.claim.notify(published); + self.cursor[slot] = Some(command); + Ok(()) + } + + /// Every cursor command the device has finished with. A used `len` of 0 + /// is no answer; one of a header is one, held to the control queue's rule. + fn drain_cursor(&mut self) -> Result<(), Refusal> { + while let Some(Used { head, written }) = used(&mut self.queues.cursor)? { + let slot = (head / CHAIN) as usize; + let command = self.cursor[slot].take().expect("virtio-gpu: toyos-virtio answered a cursor slot with nothing in flight"); + match written { + 0 => {} + HDR_BYTES => { + let answer = self.mem.read32(CURSOR_SLOT_AT + slot * CURSOR_SLOT_BYTES + CURSOR_ANSWER); + if answer != wire::RESP_OK_NODATA { + return Err(Refusal::Answered { command, answer }); + } + } + written => return Err(Refusal::CursorAnswer { command, written }), + } + } + Ok(()) + } +} + +/// The next chain `queue`'s device has finished with, or the used ring's +/// refusal under the queue's own index. +fn used(queue: &mut Virtqueue) -> Result, Refusal> { + let index = queue.index(); + queue.poll_used().map_err(|why| Refusal::Used(index, why)) +} diff --git a/system/compositor/virtio-gpu/src/tests.rs b/system/compositor/virtio-gpu/src/tests.rs new file mode 100644 index 00000000000..f1df2f2d0ed --- /dev/null +++ b/system/compositor/virtio-gpu/src/tests.rs @@ -0,0 +1,787 @@ +//! The driver against a model of the device that answers on the doorbell, or +//! on the holder's wait, with §5.7's structures as the oracle for every word +//! it is sent and every answer a device can lie in. + +use alloc::boxed::Box; +use alloc::rc::Rc; +use alloc::vec; +use alloc::vec::Vec; +use core::cell::RefCell; + +use toyos_device_memory::DmaBuffers; +use toyos_virtio::queue::{Published, UsedRefusal}; + +use crate::wire::{self, Command, Rect}; +use crate::{Claim, Declined, Gpu, Output, Queues, Refusal, CONTROL_PARTS, CONTROL_QUEUE_SIZE, CURSOR_PARTS, CURSOR_QUEUE_SIZE}; + +/// Where the device reaches the grant's first byte. +const BASE: u64 = 0x4000_0000; +/// Where the device reaches the first region the holder lends. +const LENT: u64 = 0x1_0000_0000; + +/// The grant, as both the driver and the model reach it. +#[derive(Clone)] +struct Mem(Rc>>); + +impl Mem { + fn new() -> Self { + // Anything but zero, so a part the driver forgets to clear shows. + Self(Rc::new(RefCell::new(vec![0xA5; crate::GRANT_BYTES]))) + } + + fn get(&self, at: usize, bytes: usize) -> u64 { + let mem = self.0.borrow(); + let mut word = [0u8; 8]; + word[..bytes].copy_from_slice(&mem[at..at + bytes]); + u64::from_le_bytes(word) + } + + fn set(&self, at: usize, bytes: usize, value: u64) { + self.0.borrow_mut()[at..at + bytes].copy_from_slice(&value.to_le_bytes()[..bytes]); + } + + fn words_at(&self, addr: u64, len: u32) -> Vec { + let at = (addr - BASE) as usize; + (0..len as usize / 4).map(|n| self.get(at + 4 * n, 4) as u32).collect() + } +} + +impl DmaBuffers for Mem { + fn bytes(&self) -> usize { + crate::GRANT_BYTES + } + fn device_addr(&self, at: usize) -> u64 { + BASE + at as u64 + } + fn read16(&self, at: usize) -> u16 { + self.get(at, 2) as u16 + } + fn read32(&self, at: usize) -> u32 { + self.get(at, 4) as u32 + } + fn read64(&self, at: usize) -> u64 { + self.get(at, 8) + } + fn write16(&self, at: usize, value: u16) { + self.set(at, 2, value as u64) + } + fn write32(&self, at: usize, value: u32) { + self.set(at, 4, value as u64) + } + fn write64(&self, at: usize, value: u64) { + self.set(at, 8, value) + } + fn publish(&self) {} + fn observe(&self) {} +} + +/// One command, as the device read it. +#[derive(Clone, Debug, PartialEq, Eq)] +struct Sent { + queue: u16, + code: u32, + flags: u32, + fence: u64, + payload: Vec, +} + +impl Sent { + /// The resource the command names, where §5.7.6.8 puts it. + fn resource(&self) -> u32 { + let at = match self.code { + SET_SCANOUT => 5, + FLUSH => 4, + TRANSFER => 6, + DISPLAY_INFO | GET_EDID | UPDATE_CURSOR | MOVE_CURSOR => return 0, + _ => 0, + }; + self.payload[at] + } +} + +/// What the holder and the device saw, in order. +#[derive(Clone, Debug, PartialEq, Eq)] +enum Ev { + /// A command, by its code and the resource it names (0 for none). + Command(u32, u32), + Lend(u64), + Unlend(u64), + EventsRead, + EventsClear(u32), + Wait, +} + +/// How the model answers one command. +struct Reply { + answer: u32, + flags: Option, + fence: Option, + /// The `len` it reports, where it is not the answer's own. + len: Option, +} + +fn ok() -> Reply { + Reply { answer: 0, flags: None, fence: None, len: None } +} + +/// QEMU's answer to every command: success, the fence echoed, and on the +/// cursor queue nothing at all. +fn answered(sent: &Sent) -> Reply { + let answer = match sent.code { + 0x0100 => wire::RESP_OK_DISPLAY_INFO, + 0x010a => wire::RESP_OK_EDID, + _ => wire::RESP_OK_NODATA, + }; + Reply { answer, ..ok() } +} + +type Answering = Box Reply>; + +/// The device's registers and what it shows. +struct Device { + events: u32, + /// Scanout 0's preferred size and whether it is enabled. + preferred: (u32, u32, bool), + edid: [u8; 128], + /// Raised at the next wait, as an interrupt that lands mid-command. + raise_at_wait: u32, +} + +struct State { + taken: [u16; 2], + used: [u16; 2], + sent: Vec, + trace: Vec, + /// Chains held until the next wait, where the device is slow. + deferred: bool, + held: Vec<(u16, u16)>, + /// What the holder's wait answers. + silent: bool, + /// `None` is QEMU: no cursor answer. `Some` writes this `len` and type. + cursor_answer: Option<(u32, u32)>, + device: Device, + lent: u64, + refuse_lend: bool, + answer: Answering, +} + +#[derive(Clone)] +struct Model { + mem: Mem, + state: Rc>, +} + +impl Model { + fn new() -> Self { + Self::answering(|sent, _| answered(sent)) + } + + fn answering(answer: impl FnMut(&Sent, &mut Device) -> Reply + 'static) -> Self { + let mut edid = [0u8; 128]; + edid[..8].copy_from_slice(&[0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0]); + edid[127] = 0x42; + Self { + mem: Mem::new(), + state: Rc::new(RefCell::new(State { + taken: [0; 2], + used: [0; 2], + sent: Vec::new(), + trace: Vec::new(), + deferred: false, + held: Vec::new(), + silent: false, + cursor_answer: None, + device: Device { events: 0, preferred: (1280, 800, true), edid, raise_at_wait: 0 }, + lent: 0, + refuse_lend: false, + answer: Box::new(answer), + })), + } + } + + fn parts(queue: u16) -> (crate::Parts, u16) { + match queue { + wire::CONTROL_QUEUE => (CONTROL_PARTS, CONTROL_QUEUE_SIZE), + wire::CURSOR_QUEUE => (CURSOR_PARTS, CURSOR_QUEUE_SIZE), + other => panic!("model: queue {other} was never configured"), + } + } + + /// The chain at `head`: §2.7.5's descriptors, `(addr, len, writable)`. + fn chain(&self, queue: u16, head: u16) -> Vec<(u64, u32, bool)> { + let (parts, size) = Self::parts(queue); + let mut descs = Vec::new(); + let mut at = head; + loop { + assert!(at < size && descs.len() < size as usize, "model: a chain off the table"); + let d = parts.desc + at as usize * 16; + let flags = self.mem.get(d + 12, 2); + descs.push((self.mem.get(d, 8), self.mem.get(d + 8, 4) as u32, flags & 2 != 0)); + if flags & 1 == 0 { + return descs; + } + at = self.mem.get(d + 14, 2) as u16; + } + } + + /// Take every entry made available on `queue` since the last look. + fn take(&self, queue: u16) { + let (parts, size) = Self::parts(queue); + loop { + let avail = self.mem.get(parts.avail + 2, 2) as u16; + let taken = self.state.borrow().taken[queue as usize]; + if taken == avail { + return; + } + let head = self.mem.get(parts.avail + 4 + 2 * (taken % size) as usize, 2) as u16; + self.state.borrow_mut().taken[queue as usize] = taken.wrapping_add(1); + let chain = self.chain(queue, head); + assert_eq!(chain.len(), 2, "model: every chain is a request and an answer"); + assert!(!chain[0].2 && chain[1].2, "model: §2.7.4.2: the request is read, the answer written"); + let words = self.mem.words_at(chain[0].0, chain[0].1); + let sent = Sent { + queue, + code: words[0], + flags: words[1], + fence: u64::from(words[2]) | u64::from(words[3]) << 32, + payload: words[6..].to_vec(), + }; + let mut state = self.state.borrow_mut(); + state.trace.push(Ev::Command(sent.code, sent.resource())); + state.sent.push(sent); + if state.deferred { + state.held.push((queue, head)); + } else { + drop(state); + self.answer(queue, head); + } + } + } + + /// Answer the chain at `head` and put it on the used ring. + fn answer(&self, queue: u16, head: u16) { + let chain = self.chain(queue, head); + let words = self.mem.words_at(chain[0].0, chain[0].1); + let sent = Sent { + queue, + code: words[0], + flags: words[1], + fence: u64::from(words[2]) | u64::from(words[3]) << 32, + payload: words[6..].to_vec(), + }; + let (answer_addr, room) = (chain[1].0, chain[1].1); + let len = if queue == wire::CURSOR_QUEUE { + match self.state.borrow().cursor_answer { + None => 0, + Some((len, answer)) => { + self.mem.set((answer_addr - BASE) as usize, 4, answer as u64); + len + } + } + } else { + let mut state = self.state.borrow_mut(); + let state = &mut *state; + let reply = (state.answer)(&sent, &mut state.device); + let mut out = vec![ + reply.answer, + reply.flags.unwrap_or(sent.flags), + reply.fence.unwrap_or(sent.fence) as u32, + (reply.fence.unwrap_or(sent.fence) >> 32) as u32, + 0, + 0, + ]; + if reply.answer == wire::RESP_OK_DISPLAY_INFO { + let (w, h, on) = state.device.preferred; + out.extend([0, 0, w, h, on as u32, 0]); + out.resize(wire::DISPLAY_INFO_BYTES as usize / 4, 0); + } else if reply.answer == wire::RESP_OK_EDID { + // §5.7.6.8's `size`, which QEMU answers with the buffer's 1024. + out.extend([1024, 0]); + out.extend(state.device.edid.chunks(4).map(|w| u32::from_le_bytes(w.try_into().unwrap()))); + out.resize(wire::EDID_BYTES as usize / 4, 0); + } + // A device writes no more than the room it was given. + out.truncate(room as usize / 4); + let bytes = (out.len() * 4) as u32; + for (nth, word) in out.iter().enumerate() { + self.mem.set((answer_addr - BASE) as usize + 4 * nth, 4, *word as u64); + } + reply.len.unwrap_or(bytes) + }; + let (parts, size) = Self::parts(queue); + let used = self.state.borrow().used[queue as usize]; + let at = parts.used + 4 + (used % size) as usize * 8; + self.mem.set(at, 4, head as u64); + self.mem.set(at + 4, 4, len as u64); + let used = used.wrapping_add(1); + self.state.borrow_mut().used[queue as usize] = used; + self.mem.set(parts.used + 2, 2, used as u64); + } + + fn trace(&self) -> Vec { + self.state.borrow().trace.clone() + } + + fn forget(&self) { + let mut state = self.state.borrow_mut(); + state.trace.clear(); + state.sent.clear(); + } + + fn sent(&self) -> Vec { + self.state.borrow().sent.clone() + } + + fn with(&self, f: impl FnOnce(&mut State)) { + f(&mut self.state.borrow_mut()); + } +} + +/// The holder: the model's doorbell, configuration, interrupt and lender. +struct Holder(Model); + +impl Claim for Holder { + type Backing = u64; + type Refused = (); + + fn notify(&self, published: Published) { + self.0.take(published.queue()); + } + + fn events_read(&mut self) -> Result { + let mut state = self.0.state.borrow_mut(); + state.trace.push(Ev::EventsRead); + Ok(state.device.events) + } + + fn events_clear(&mut self, events: u32) -> Result<(), toyos_virtio::pci::Refusal> { + let mut state = self.0.state.borrow_mut(); + state.trace.push(Ev::EventsClear(events)); + state.device.events &= !events; + Ok(()) + } + + fn wait(&mut self) -> bool { + let held = { + let mut state = self.0.state.borrow_mut(); + state.trace.push(Ev::Wait); + if state.silent { + return false; + } + state.device.events |= core::mem::take(&mut state.device.raise_at_wait); + core::mem::take(&mut state.held) + }; + for (queue, head) in held { + self.0.answer(queue, head); + } + true + } + + fn lend(&mut self, bytes: u64) -> Result<(u64, u64), ()> { + let mut state = self.0.state.borrow_mut(); + if state.refuse_lend { + return Err(()); + } + let addr = LENT + state.lent; + state.lent += bytes.next_multiple_of(0x20_0000); + state.trace.push(Ev::Lend(addr)); + Ok((addr, addr)) + } + + fn unlend(&mut self, backing: u64) { + self.0.state.borrow_mut().trace.push(Ev::Unlend(backing)); + } +} + +type Driver = Gpu; + +fn open(model: &Model) -> Driver { + let queues = Queues::new(model.mem.clone()); + Gpu::open(queues, model.mem.clone(), Holder(model.clone()), true) +} + +/// A driver with `mode` on screen, and the model's record forgotten. +fn showing(model: &Model, mode: (u32, u32)) -> Driver { + let mut gpu = open(model); + flip(&mut gpu, mode).expect("the model answers").expect("the model takes the mode"); + model.forget(); + gpu +} + +fn flip(gpu: &mut Driver, mode: (u32, u32)) -> Result>, Refusal> { + match gpu.prepare(mode)? { + Ok(next) => gpu.show(next), + Err(declined) => Ok(Err(declined)), + } +} + +const CREATE: u32 = 0x0101; +const UNREF: u32 = 0x0102; +const SET_SCANOUT: u32 = 0x0103; +const FLUSH: u32 = 0x0104; +const TRANSFER: u32 = 0x0105; +const ATTACH: u32 = 0x0106; +const DISPLAY_INFO: u32 = 0x0100; +const GET_EDID: u32 = 0x010a; +const UPDATE_CURSOR: u32 = 0x0300; +const MOVE_CURSOR: u32 = 0x0301; + +// --- §5.7.4.2 and §5.7.5: the output, and its events --- + +/// §5.7.5: the output is the display info's preferred size and the EDID, read +/// after the events are cleared. +#[test] +fn the_output_is_read_after_the_events_are_cleared() { + let model = Model::new(); + model.with(|s| s.device.events = wire::EVENT_DISPLAY); + let mut gpu = open(&model); + let output = gpu.output().expect("the model answers"); + assert_eq!( + model.trace(), + [ + Ev::EventsRead, + Ev::EventsClear(wire::EVENT_DISPLAY), + Ev::Command(DISPLAY_INFO, 0), + Ev::Command(GET_EDID, 0), + ] + ); + let mut block = [0u8; 128]; + block[..8].copy_from_slice(&[0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0]); + block[127] = 0x42; + assert_eq!(output, Output { preferred: Some((1280, 800)), edid: Some(block) }); + assert!(!gpu.display_changed()); +} + +/// An event the device raises while the driver reads the output is not +/// cleared by it: the next wake sees it. +#[test] +fn an_event_raised_during_the_reads_is_seen_on_the_next_wake() { + let model = Model::answering(|sent, device| { + if sent.code == DISPLAY_INFO { + device.events |= wire::EVENT_DISPLAY; + } + answered(sent) + }); + let mut gpu = open(&model); + gpu.output().expect("the model answers"); + assert!(!gpu.display_changed()); + gpu.wake().expect("the model answers"); + assert!(gpu.display_changed(), "the event raised between the clear and the reads was lost"); +} + +#[test] +fn a_disabled_scanout_has_no_preferred_size_and_no_edid_is_asked_without_the_feature() { + let model = Model::new(); + model.with(|s| s.device.preferred = (1024, 768, false)); + let queues = Queues::new(model.mem.clone()); + let mut gpu = Gpu::open(queues, model.mem.clone(), Holder(model.clone()), false); + assert_eq!(gpu.output(), Ok(Output { preferred: None, edid: None })); + assert!(!model.trace().contains(&Ev::Command(GET_EDID, 0))); +} + +/// One interrupt serves the queues and the configuration, so an event that +/// lands while a frame's transfers are awaited is noted by that wait. +#[test] +fn an_event_raised_while_a_frames_transfer_is_awaited_is_read_on_the_next_pass() { + let model = Model::new(); + let mut gpu = showing(&model, (1280, 800)); + model.with(|s| { + s.deferred = true; + s.device.raise_at_wait = wire::EVENT_DISPLAY; + }); + gpu.present(&[Rect { x: 0, y: 0, width: 16, height: 16 }]).expect("the model answers"); + assert!(gpu.display_changed(), "a display event the frame's wait took was lost"); +} + +// --- §5.7.6.5 and §5.7.6.7: fences and answers --- + +/// Every control command carries the fence flag and an id of its own; no +/// cursor command does. +#[test] +fn every_control_command_is_fenced_with_its_own_id() { + let model = Model::new(); + let mut gpu = open(&model); + gpu.output().expect("the model answers"); + flip(&mut gpu, (1280, 800)).expect("the model answers").expect("taken"); + flip(&mut gpu, (1600, 900)).expect("the model answers").expect("taken"); + gpu.present(&[Rect { x: 1, y: 2, width: 3, height: 4 }]).expect("the model answers"); + gpu.cursor(&[0; 4096], (0, 0), (5, 5)).expect("the model answers"); + gpu.move_cursor((6, 6)).expect("the model answers"); + let sent = model.sent(); + let control: Vec<&Sent> = sent.iter().filter(|s| s.queue == wire::CONTROL_QUEUE).collect(); + assert!(control.len() > 10); + assert!(control.iter().all(|s| s.flags == wire::FLAG_FENCE), "{control:?}"); + let fences: Vec = control.iter().map(|s| s.fence).collect(); + assert!(fences.windows(2).all(|w| w[0] < w[1]), "fences repeat: {fences:?}"); + let cursor: Vec<&Sent> = sent.iter().filter(|s| s.queue == wire::CURSOR_QUEUE).collect(); + assert_eq!(cursor.iter().map(|s| s.code).collect::>(), [UPDATE_CURSOR, MOVE_CURSOR]); + assert!(cursor.iter().all(|s| s.flags == 0)); +} + +#[test] +fn an_answer_without_the_fence_or_with_another_id_is_refused() { + let model = Model::answering(|sent, _| Reply { flags: Some(0), ..answered(sent) }); + let mut gpu = open(&model); + assert!(matches!( + gpu.output(), + Err(Refusal::Unfenced { command: Command::GetDisplayInfo, flags: 0, .. }) + )); + + let model = Model::answering(|sent, _| Reply { fence: Some(sent.fence + 1), ..answered(sent) }); + let mut gpu = open(&model); + assert_eq!( + gpu.output(), + Err(Refusal::Unfenced { command: Command::GetDisplayInfo, flags: wire::FLAG_FENCE, fence: 2, sent: 1 }) + ); +} + +#[test] +fn an_answer_shorter_than_its_structure_is_refused() { + let model = Model::answering(|sent, _| Reply { len: Some(wire::HDR_BYTES), ..answered(sent) }); + let mut gpu = open(&model); + assert_eq!( + gpu.output(), + Err(Refusal::ShortAnswer { + command: Command::GetDisplayInfo, + written: wire::HDR_BYTES, + wanted: wire::DISPLAY_INFO_BYTES + }) + ); + let model = Model::answering(|sent, _| Reply { len: Some(8), ..answered(sent) }); + let mut gpu = open(&model); + assert_eq!( + flip(&mut gpu, (800, 600)), + Err(Refusal::ShortAnswer { command: Command::ResourceCreate2d, written: 8, wanted: wire::HDR_BYTES }) + ); +} + +#[test] +fn a_device_that_never_answers_is_refused_by_the_command_it_owes() { + let model = Model::new(); + let mut gpu = showing(&model, (800, 600)); + model.with(|s| { + s.deferred = true; + s.silent = true; + }); + assert_eq!( + gpu.present(&[Rect::whole(800, 600)]), + Err(Refusal::Silent(Command::TransferToHost2d)) + ); +} + +// --- §5.7.6.3: the page flip --- + +/// The new scanout is made and filled while the old one is on screen, and the +/// old one's backing is given back only once its `UNREF` is answered, after +/// `SET_SCANOUT`. +#[test] +fn a_flip_unlends_the_old_backing_only_after_set_scanout_and_its_unref() { + let model = Model::new(); + let mut gpu = showing(&model, (1280, 800)); + let old = *gpu.backing().expect("on screen"); + let next = gpu.prepare((1600, 900)).expect("the model answers").expect("taken"); + let new = *next.backing(); + assert_eq!(gpu.mode(), Some((1280, 800)), "the old mode stays until the new one is shown"); + gpu.show(next).expect("the model answers").expect("taken"); + assert_eq!(gpu.mode(), Some((1600, 900))); + assert_eq!(gpu.backing(), Some(&new)); + assert_eq!( + model.trace(), + [ + Ev::Lend(new), + Ev::Command(CREATE, 3), + Ev::Command(ATTACH, 3), + Ev::Command(TRANSFER, 3), + Ev::Command(SET_SCANOUT, 3), + Ev::Command(FLUSH, 3), + Ev::Command(UNREF, 2), + Ev::Unlend(old), + ] + ); + let sent = model.sent(); + assert_eq!(sent[1].payload, [3, 1, new as u32, (new >> 32) as u32, 1600 * 900 * 4, 0]); + assert_eq!(sent[2].payload, [0, 0, 1600, 900, 0, 0, 3, 0]); + assert_eq!(sent[3].payload, [0, 0, 1600, 900, 0, 3]); +} + +/// §5.7.6.7's `RESP_ERR_OUT_OF_MEMORY` at each of the three steps that make +/// a scanout declines the mode: the old one stays, and the new one's +/// resource and backing are given back. +#[test] +fn out_of_memory_at_each_step_keeps_the_old_mode() { + for (refused, command, after) in [ + (CREATE, Command::ResourceCreate2d, &[][..]), + (ATTACH, Command::ResourceAttachBacking, &[UNREF][..]), + (SET_SCANOUT, Command::SetScanout, &[UNREF][..]), + ] { + let model = Model::answering(move |sent, _| match (sent.code, sent.resource()) { + (code, 3) if code == refused => Reply { answer: wire::RESP_ERR_OUT_OF_MEMORY, ..ok() }, + _ => answered(sent), + }); + let mut gpu = showing(&model, (1280, 800)); + let old = *gpu.backing().expect("on screen"); + assert_eq!(flip(&mut gpu, (1600, 900)), Ok(Err(Declined::Device(command))), "{}", command.name()); + assert_eq!(gpu.mode(), Some((1280, 800))); + assert_eq!(gpu.backing(), Some(&old)); + let trace = model.trace(); + let lent = match trace[0] { + Ev::Lend(at) => at, + ref other => panic!("{}: the flip began with {other:?}", command.name()), + }; + let at = trace.iter().position(|e| *e == Ev::Command(refused, 3)).expect("the refused command was sent"); + let tail: Vec = after.iter().map(|code| Ev::Command(*code, 3)).chain([Ev::Unlend(lent)]).collect(); + assert_eq!(trace[at + 1..], tail[..], "{}", command.name()); + assert!(!trace.contains(&Ev::Unlend(old))); + } +} + +#[test] +fn a_lend_refused_declines_the_mode_before_the_device_hears_of_it() { + let model = Model::new(); + let mut gpu = showing(&model, (1280, 800)); + model.with(|s| s.refuse_lend = true); + assert_eq!(flip(&mut gpu, (1600, 900)), Ok(Err(Declined::Lend(())))); + assert!(model.trace().is_empty()); + assert_eq!(gpu.mode(), Some((1280, 800))); +} + +/// Every other error answer, to any command, is a fault and ends the device. +#[test] +fn any_other_error_answer_is_refused_by_name() { + let model = Model::answering(|sent, _| match sent.code { + CREATE => Reply { answer: wire::RESP_ERR_INVALID_PARAMETER, ..ok() }, + _ => answered(sent), + }); + let mut gpu = open(&model); + assert_eq!( + flip(&mut gpu, (800, 600)), + Err(Refusal::Answered { command: Command::ResourceCreate2d, answer: wire::RESP_ERR_INVALID_PARAMETER }) + ); + + let model = Model::new(); + let mut gpu = showing(&model, (800, 600)); + model.with(|s| s.answer = Box::new(|sent, _| match sent.code { + TRANSFER => Reply { answer: wire::RESP_ERR_OUT_OF_MEMORY, ..ok() }, + _ => answered(sent), + })); + assert_eq!( + gpu.present(&[Rect::whole(10, 10)]), + Err(Refusal::Answered { command: Command::TransferToHost2d, answer: wire::RESP_ERR_OUT_OF_MEMORY }) + ); + + let model = Model::new(); + let mut gpu = showing(&model, (800, 600)); + model.with(|s| s.answer = Box::new(|sent, _| match sent.code { + UNREF => Reply { answer: wire::RESP_ERR_INVALID_RESOURCE_ID, ..ok() }, + _ => answered(sent), + })); + assert_eq!( + flip(&mut gpu, (1024, 768)), + Err(Refusal::Answered { command: Command::ResourceUnref, answer: wire::RESP_ERR_INVALID_RESOURCE_ID }) + ); + + let model = Model::answering(|sent, _| Reply { answer: wire::RESP_OK_EDID, ..answered(sent) }); + let mut gpu = open(&model); + assert_eq!( + gpu.output(), + Err(Refusal::Answered { command: Command::GetDisplayInfo, answer: wire::RESP_OK_EDID }) + ); +} + +// --- §5.7.6.2: a present --- + +/// Every rectangle's transfer is answered before any flush is sent, each at +/// the offset of its first byte in a row-major backing. +#[test] +fn a_present_transfers_every_rectangle_before_it_flushes_any() { + let model = Model::new(); + let mut gpu = showing(&model, (1280, 800)); + let rects = [Rect { x: 10, y: 20, width: 30, height: 40 }, Rect { x: 0, y: 799, width: 1280, height: 1 }]; + gpu.present(&rects).expect("the model answers"); + let codes: Vec = model.sent().iter().map(|s| s.code).collect(); + assert_eq!(codes, [TRANSFER, TRANSFER, FLUSH, FLUSH]); + let offset = |x: u64, y: u64| (y * 1280 + x) * 4; + assert_eq!(model.sent()[0].payload, [10, 20, 30, 40, offset(10, 20) as u32, 0, 2, 0]); + assert_eq!(model.sent()[1].payload, [0, 799, 1280, 1, offset(0, 799) as u32, 0, 2, 0]); + assert_eq!(model.sent()[3].payload, [0, 799, 1280, 1, 2, 0]); +} + +/// More rectangles than control slots: a slot is reused once its command is +/// answered. +#[test] +fn a_present_wider_than_the_queue_waits_for_slots() { + let model = Model::new(); + let mut gpu = showing(&model, (1280, 800)); + model.with(|s| s.deferred = true); + let rects: Vec = (0..20).map(|y| Rect { x: 0, y, width: 8, height: 1 }).collect(); + gpu.present(&rects).expect("the model answers"); + assert_eq!(model.sent().len(), 40); + assert!(model.trace().contains(&Ev::Wait)); +} + +#[test] +#[should_panic(expected = "outside the 1280x800 scanout")] +fn a_rectangle_outside_the_scanout_is_the_callers_mistake() { + let model = Model::new(); + let mut gpu = showing(&model, (1280, 800)); + let _ = gpu.present(&[Rect { x: 1270, y: 0, width: 11, height: 1 }]); +} + +// --- §5.7.6.6 and §5.7.6.10: the cursor --- + +/// The cursor's resource is made once, its image transferred and answered +/// before `UPDATE_CURSOR`, and QEMU's cursor queue answers nothing (used +/// `len` 0), which is no answer. +#[test] +fn the_cursor_is_filled_and_fenced_before_it_is_shown() { + let model = Model::new(); + let mut gpu = showing(&model, (800, 600)); + let mut image = [0u32; 4096]; + image[0] = 0x8011_2233; + gpu.cursor(&image, (3, 4), (100, 200)).expect("the model answers"); + gpu.cursor(&image, (3, 4), (100, 200)).expect("the model answers"); + let codes: Vec = model.sent().iter().map(|s| s.code).collect(); + assert_eq!(codes, [CREATE, ATTACH, TRANSFER, UPDATE_CURSOR, TRANSFER, UPDATE_CURSOR]); + assert_eq!(model.sent()[0].payload, [1, wire::FORMAT_B8G8R8A8_UNORM, 64, 64]); + assert_eq!(model.sent()[3].payload, [0, 100, 200, 0, 1, 3, 4, 0]); + let attach = &model.sent()[1].payload; + let image_at = u64::from(attach[2]) | u64::from(attach[3]) << 32; + assert_eq!(model.mem.words_at(image_at, 4), [0x8011_2233]); +} + +#[test] +fn a_cursor_answer_is_read_only_at_a_headers_length() { + for (answer, want) in [ + (Some((wire::HDR_BYTES, wire::RESP_OK_NODATA)), Ok(())), + ( + Some((wire::HDR_BYTES, wire::RESP_ERR_INVALID_RESOURCE_ID)), + Err(Refusal::Answered { command: Command::MoveCursor, answer: wire::RESP_ERR_INVALID_RESOURCE_ID }), + ), + (Some((8, wire::RESP_OK_NODATA)), Err(Refusal::CursorAnswer { command: Command::MoveCursor, written: 8 })), + ] { + let model = Model::new(); + let mut gpu = showing(&model, (800, 600)); + model.with(|s| s.cursor_answer = answer); + gpu.move_cursor((1, 1)).expect("published"); + assert_eq!(gpu.wake(), want, "{answer:?}"); + } + // Past the header's 24 bytes is more than the chain may be written. + let model = Model::new(); + let mut gpu = showing(&model, (800, 600)); + model.with(|s| s.cursor_answer = Some((25, wire::RESP_OK_NODATA))); + gpu.move_cursor((1, 1)).expect("published"); + assert!(matches!(gpu.wake(), Err(Refusal::Used(wire::CURSOR_QUEUE, UsedRefusal::Written { .. })))); +} + +/// A cursor queue whose slots are all the device's waits for one. +#[test] +fn a_full_cursor_queue_waits_for_a_slot() { + let model = Model::new(); + let mut gpu = showing(&model, (800, 600)); + model.with(|s| s.deferred = true); + for x in 0..9 { + gpu.move_cursor((x, 0)).expect("the model answers"); + } + assert_eq!(model.trace().iter().filter(|e| **e == Ev::Wait).count(), 1); + assert_eq!(model.sent().len(), 9); +} diff --git a/system/compositor/virtio-gpu/src/wire.rs b/system/compositor/virtio-gpu/src/wire.rs new file mode 100644 index 00000000000..100bee365b1 --- /dev/null +++ b/system/compositor/virtio-gpu/src/wire.rs @@ -0,0 +1,148 @@ +//! The GPU device's messages (§5.7.6): their codes, and each structure as the +//! little-endian words it is on the wire. +//! +//! **Every structure here is a whole number of `le32` words**, a `le64` as +//! its low word then its high one, so a message is written into the grant and +//! read out of it as words. + +/// §5.7.2: the queues, by index. +pub const CONTROL_QUEUE: u16 = 0; +pub const CURSOR_QUEUE: u16 = 1; + +/// §5.7.3: the one feature bit this driver takes. +pub const F_EDID: u64 = 1 << 1; + +/// §5.7.4: the configuration's fields, by offset, and the one event. +pub const EVENTS_READ: usize = 0; +pub const EVENTS_CLEAR: usize = 4; +pub const NUM_SCANOUTS: usize = 8; +pub const EVENT_DISPLAY: u32 = 1 << 0; + +/// §5.7.6.7: the answers this driver reads. +pub const RESP_OK_NODATA: u32 = 0x1100; +pub const RESP_OK_DISPLAY_INFO: u32 = 0x1101; +pub const RESP_OK_EDID: u32 = 0x1104; +pub const RESP_ERR_UNSPEC: u32 = 0x1200; +pub const RESP_ERR_OUT_OF_MEMORY: u32 = 0x1201; +pub const RESP_ERR_INVALID_SCANOUT_ID: u32 = 0x1202; +pub const RESP_ERR_INVALID_RESOURCE_ID: u32 = 0x1203; +pub const RESP_ERR_INVALID_CONTEXT_ID: u32 = 0x1204; +pub const RESP_ERR_INVALID_PARAMETER: u32 = 0x1205; + +/// §5.7.6.7: `VIRTIO_GPU_FLAG_FENCE`. +pub const FLAG_FENCE: u32 = 1 << 0; + +/// §5.7.6.8: the two formats this driver creates resources in, whose bytes +/// in memory are blue, green, red, then alpha or nothing. +pub const FORMAT_B8G8R8A8_UNORM: u32 = 1; +pub const FORMAT_B8G8R8X8_UNORM: u32 = 2; + +/// §5.7.6.6: a cursor's resource is 64x64. +pub const CURSOR_SIDE: u32 = 64; + +/// Bytes of each structure: §5.7.6.7's header, and the answers with data +/// (§5.7.6.8): sixteen scanouts' `virtio_gpu_display_one`, and the EDID's +/// `size`, `padding` and 1024 bytes. +pub const HDR_BYTES: u32 = 24; +pub const DISPLAY_INFO_BYTES: u32 = HDR_BYTES + 16 * 24; +pub const EDID_BYTES: u32 = HDR_BYTES + 8 + 1024; +/// Where the EDID's own bytes start in its answer. +pub const EDID_AT: usize = HDR_BYTES as usize + 8; + +/// A command this driver sends, by what it is called. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Command { + GetDisplayInfo, + ResourceCreate2d, + ResourceUnref, + SetScanout, + ResourceFlush, + TransferToHost2d, + ResourceAttachBacking, + GetEdid, + UpdateCursor, + MoveCursor, +} + +impl Command { + /// §5.7.6.7's `virtio_gpu_ctrl_type`. + pub fn code(self) -> u32 { + match self { + Self::GetDisplayInfo => 0x0100, + Self::ResourceCreate2d => 0x0101, + Self::ResourceUnref => 0x0102, + Self::SetScanout => 0x0103, + Self::ResourceFlush => 0x0104, + Self::TransferToHost2d => 0x0105, + Self::ResourceAttachBacking => 0x0106, + Self::GetEdid => 0x010a, + Self::UpdateCursor => 0x0300, + Self::MoveCursor => 0x0301, + } + } + + pub fn name(self) -> &'static str { + match self { + Self::GetDisplayInfo => "GET_DISPLAY_INFO", + Self::ResourceCreate2d => "RESOURCE_CREATE_2D", + Self::ResourceUnref => "RESOURCE_UNREF", + Self::SetScanout => "SET_SCANOUT", + Self::ResourceFlush => "RESOURCE_FLUSH", + Self::TransferToHost2d => "TRANSFER_TO_HOST_2D", + Self::ResourceAttachBacking => "RESOURCE_ATTACH_BACKING", + Self::GetEdid => "GET_EDID", + Self::UpdateCursor => "UPDATE_CURSOR", + Self::MoveCursor => "MOVE_CURSOR", + } + } + + /// The answer it succeeds with, and how many bytes that answer is. + pub fn ok(self) -> (u32, u32) { + match self { + Self::GetDisplayInfo => (RESP_OK_DISPLAY_INFO, DISPLAY_INFO_BYTES), + Self::GetEdid => (RESP_OK_EDID, EDID_BYTES), + _ => (RESP_OK_NODATA, HDR_BYTES), + } + } +} + +/// An answer's name, where §5.7.6.7 gives it one. +pub fn answer_name(code: u32) -> Option<&'static str> { + Some(match code { + RESP_OK_NODATA => "RESP_OK_NODATA", + RESP_OK_DISPLAY_INFO => "RESP_OK_DISPLAY_INFO", + RESP_OK_EDID => "RESP_OK_EDID", + RESP_ERR_UNSPEC => "RESP_ERR_UNSPEC", + RESP_ERR_OUT_OF_MEMORY => "RESP_ERR_OUT_OF_MEMORY", + RESP_ERR_INVALID_SCANOUT_ID => "RESP_ERR_INVALID_SCANOUT_ID", + RESP_ERR_INVALID_RESOURCE_ID => "RESP_ERR_INVALID_RESOURCE_ID", + RESP_ERR_INVALID_CONTEXT_ID => "RESP_ERR_INVALID_CONTEXT_ID", + RESP_ERR_INVALID_PARAMETER => "RESP_ERR_INVALID_PARAMETER", + _ => return None, + }) +} + +/// §5.7.6.8's `virtio_gpu_rect`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Rect { + pub x: u32, + pub y: u32, + pub width: u32, + pub height: u32, +} + +impl Rect { + pub const fn whole(width: u32, height: u32) -> Self { + Self { x: 0, y: 0, width, height } + } + + pub fn words(self) -> [u32; 4] { + [self.x, self.y, self.width, self.height] + } +} + +/// §5.7.6.7's header: `type`, `flags`, `fence_id`, `ctx_id`, then `ring_idx` +/// and its padding, which this driver leaves 0. +pub fn header(command: Command, flags: u32, fence: u64) -> [u32; 6] { + [command.code(), flags, fence as u32, (fence >> 32) as u32, 0, 0] +} diff --git a/system/console/src/main.rs b/system/console/src/main.rs index fe5208f3115..5914d4ca44c 100644 --- a/system/console/src/main.rs +++ b/system/console/src/main.rs @@ -132,15 +132,15 @@ impl Log { }); } - /// Draw what is held; whether there was any. + /// Draw what is held. /// /// **Above the shell's unfinished line, never inside it**: where the shell /// is part of the way through one — its prompt, and what is being typed at /// it — that row is cleared, the lines drawn, and the shell's line drawn /// again under them. `partial` is that line as the shell wrote it. - fn draw(&mut self, partial: &[u8], console: &mut Console) -> bool { + fn draw(&mut self, partial: &[u8], console: &mut Console) { if self.held.is_empty() { - return false; + return; } if !partial.is_empty() { console.write_bytes(b"\r\x1b[K"); @@ -148,7 +148,6 @@ impl Log { console.write_bytes(&self.held); console.write_bytes(partial); self.held.clear(); - true } } @@ -175,7 +174,7 @@ fn main() { return; }; let info = fb_dev.info().expect("console: framebuffer info"); - let shm = SharedMemory::adopt(info.scanout[0], info.stride as usize * info.height as usize * 4) + let shm = SharedMemory::adopt(info.scanout, info.stride as usize * info.height as usize * 4) .expect("console: the scanout buffer the framebuffer claim just handed over"); let screen = Screen::new( shm.as_ptr(), @@ -202,7 +201,6 @@ fn main() { 0 } }; - present(&fb_dev, info.width, info.height); let kb: Keyboard = endow::device(DeviceType::Keyboard) .expect("the manifest gives this program the keyboard"); @@ -264,8 +262,6 @@ fn main() { } }); - let mut painted = false; - if ready[TOKEN_STDOUT as usize] { let mut buf = [0u8; 4096]; match shell.stdout.read(&mut buf).unwrap_or(0) { @@ -277,12 +273,10 @@ fn main() { shell.restart(&connector); console.write_bytes(b"\n[console] the shell exited; a new one is running\n"); partial.clear(); - painted = true; } n => { console.write_bytes(&buf[..n]); unfinished(&mut partial, &buf[..n]); - painted = true; } } } @@ -293,7 +287,6 @@ fn main() { if n > 0 { console.write_bytes(&buf[..n]); unfinished(&mut partial, &buf[..n]); - painted = true; } } @@ -312,7 +305,7 @@ fn main() { } } if let Ok(reader) = &mut log { - painted |= reader.draw(&partial, &mut console); + reader.draw(&partial, &mut console); } if ready[TOKEN_LISTEN as usize] { @@ -418,11 +411,9 @@ fn main() { // does not reach for with a laptop in his hands. KEY_PAGE_UP => { console.scroll_view_up(page_rows); - painted = true; } KEY_PAGE_DOWN => { console.scroll_view_down(page_rows); - painted = true; } usage => { let text = translator.press(usage, window::KeyEvent::from(event).mods()); @@ -432,10 +423,6 @@ fn main() { } } } - - if painted { - present(&fb_dev, info.width, info.height); - } } } @@ -499,14 +486,6 @@ fn seed_tail(log: &[u8]) -> &[u8] { window } -/// One present per drained batch, never per byte. Free on a GOP framebuffer — -/// `gop.rs`'s `present_rect` is empty, because the scanout *is* the memory just -/// written — and one transfer per batch on virtio-gpu, the only backend where -/// it costs anything. -fn present(fb: &FramebufferDev, width: u32, height: u32) { - fb.present(0, 0, width, height).expect("console holds the framebuffer claim"); -} - struct Shell { child: Child, stdin: ChildStdin, diff --git a/tests/common/iommu.rs b/tests/common/iommu.rs index 57a2dd6c83c..008b16506d8 100644 --- a/tests/common/iommu.rs +++ b/tests/common/iommu.rs @@ -30,9 +30,8 @@ fn netcase() -> std::path::PathBuf { /// the NIC's and the sound function's drivers are processes', whose /// negotiation is `toyos-virtio`'s and host-tested there. On [`Profile::HeadlessNoIommu`] /// the guest reports `n` because QEMU never *offers* the bit -/// (`hw/virtio/virtio-bus.c:87-94`), not because the driver declined — it offers -/// blindly; the independence comes from [`declining_is_not_free`]. -pub fn iommu_virtio_platform(test_config: &Path) -> Result<(), String> { +/// (`hw/virtio/virtio-bus.c:87-94`), not because the driver declined. +pub fn iommu_virtio_platform() -> Result<(), String> { for profile in [Profile::Headless, Profile::HeadlessNoIommu] { let name = if profile.iommu().is_some() { "headless" } else { "headless-no-iommu" }; let behind_unit = profile.iommu().is_some(); @@ -149,7 +148,7 @@ pub fn iommu_virtio_platform(test_config: &Path) -> Result<(), String> { if behind_unit { "the NIC's claim handed over" } else { "every claim refused for want of a unit" } ); } - declining_is_not_free(test_config) + Ok(()) } /// The slot QEMU's `-device` order puts `tests/images/netcase.toml`'s NVMe controller on, @@ -193,49 +192,6 @@ fn no_unit_is_no_claim(log: &Serial) -> Result<(), String> { Ok(()) } -/// The control that makes the two arms above mean something: a guest that -/// declines the feature its host offered gets no device, not a bypassing one. -/// `virtio_validate_features` returns `-EFAULT` and `virtio_set_status` returns -/// before it stores the status (`hw/virtio/virtio.c:2270-2276` and `:2292-2299` -/// at v11.1.1), so `FEATURES_OK` never sticks. The actuator withholds the bit -/// from every virtio device the kernel drives but the console — on this -/// machine the virtio-gpu it adds for that, since every other function is a -/// process's claim — and each of them is refused for it. -fn declining_is_not_free(test_config: &Path) -> Result<(), String> { - let qemu = QemuInstance::boot_with_options( - test_config, - &[], - &[], - BootOptions { - profile: Profile::HeadlessVirtioGpu, - kernel_params: &["virtio-no-access-platform"], - ..Default::default() - }, - ); - let log = Serial::boot(&qemu); - log.must_be_clean()?; - log.must_say("Boot: complete")?; - - let refused: Vec<&str> = log - .text() - .lines() - .filter(|l| l.contains("refused the feature set")) - .collect(); - if refused.is_empty() { - return Err(format!( - "the actuator withheld VIRTIO_F_ACCESS_PLATFORM from every virtio device but the \ - console and none was refused for it. A device the host offered it on and the guest \ - declined has to lose FEATURES_OK, and this machine went on as though the \ - negotiation were free\n{}", - log.text() - )); - } - // The console kept the bit, so this is not simply a machine with no virtio. - log.must_say("access_platform=y")?; - refused.iter().for_each(|line| eprintln!(" [iommu] declined: {line}")); - Ok(()) -} - /// The slot QEMU's `-device` order puts the function netstack claims on, and the /// address every judge below is an assertion about. /// diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 0fe40a9630f..baa48f0370f 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -20,3 +20,5 @@ 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/qemu.rs b/tests/common/qemu.rs index 4c020f44dd6..3a5b31d1145 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -681,9 +681,9 @@ pub enum Profile { /// disk is the only storage it has, and the only device its firmware can /// boot. HeadlessNoUsb, - /// [`Profile::Headless`] with a virtio-gpu behind the unit: a virtio - /// function the kernel still drives itself beside the console, which is - /// what `virtio-no-access-platform` withholds the bit from. + /// [`Profile::Headless`] with a virtio-gpu behind the unit: the suite's + /// one machine whose display a process drives, the compositor through + /// its claim. HeadlessVirtioGpu, /// [`Profile::Headless`] with a second controller, QEMU's `qemu-xhci` /// on MSI and with no MSI-X, carrying a stick and a keyboard: the @@ -929,7 +929,7 @@ struct Shape { /// RNDR for firmware or the kernel to draw from; a q35's firmware answers /// the protocol from RDRAND without one. rng: bool, - /// A virtio-gpu, which the kernel drives. + /// A virtio-gpu, which the compositor drives. virtio_gpu: bool, } @@ -1076,6 +1076,9 @@ pub struct BootOptions { /// Open a per-instance QMP socket, which `screendump` needs. Per-instance /// because screen tests boot their own QEMU and several may exist at once. pub qmp: bool, + /// Serve the display over a per-instance VNC socket: a host window a + /// client resizes ([`super::vnc::resize`]). + pub vnc: bool, /// Which of [`DECLARED_KERNEL_BUILDS`] this boot wants, and empty for the /// kernel an image ships. Only a test whose subject *is* a build sets it; /// everything else names an actuator in [`BootOptions::kernel_params`] @@ -1140,6 +1143,7 @@ impl Default for BootOptions { smp: 2, profile: Profile::Headless, qmp: false, + vnc: false, kernel_features: &[], kernel_params: &[], mute: false, @@ -1652,6 +1656,10 @@ impl QemuInstance { self.sockets.qmp.as_deref().expect("qmp_socket needs BootOptions { qmp: true }") } + pub fn vnc_socket(&self) -> &Path { + self.sockets.vnc.as_deref().expect("vnc_socket needs BootOptions { vnc: true }") + } + pub fn run_test(&mut self, name: &str, timeout: Duration) -> TestResult { writeln!(self.stdin, "run {name}").expect("Failed to write to QEMU stdin"); self.stdin.flush().expect("Failed to flush QEMU stdin"); @@ -2237,6 +2245,9 @@ fn qemu_command( (Arch::Aarch64, other) => panic!("`virt` has no `-vga {other}`"), } qemu.arg("-display").arg("none").arg("-no-reboot"); + if let Some(socket) = vnc_socket(socket_dir, options) { + qemu.arg("-vnc").arg(format!("unix:{}", socket.display())); + } if let Some(base) = options.rtc_base { qemu.arg("-rtc").arg(format!("base={base}")); } @@ -2388,13 +2399,15 @@ fn qemu_command( struct Sockets { dir: TempDir, qmp: Option, + vnc: Option, } impl Sockets { fn new(options: &BootOptions) -> Sockets { let dir = TempDir::short("boot"); let qmp = qmp_socket(&dir, options); - Sockets { dir, qmp } + let vnc = vnc_socket(&dir, options); + Sockets { dir, qmp, vnc } } } @@ -2403,6 +2416,11 @@ fn qmp_socket(dir: &Path, options: &BootOptions) -> Option { options.qmp.then(|| dir.join("qmp.sock")) } +/// The VNC socket `options` asks for, named in `dir`. +fn vnc_socket(dir: &Path, options: &BootOptions) -> Option { + options.vnc.then(|| dir.join("vnc.sock")) +} + /// Every file one boot owns, so that adding another does not lengthen a /// parameter list eight paths long. struct Files { diff --git a/tests/common/vnc.rs b/tests/common/vnc.rs new file mode 100644 index 00000000000..56511a3675b --- /dev/null +++ b/tests/common/vnc.rs @@ -0,0 +1,74 @@ +//! As much of a VNC client as resizing the host's window takes: RFC 6143's +//! handshake with no security, then the RFB community specification's +//! `SetDesktopSize` (client message 251), which a client sends once it has +//! said it takes the `ExtendedDesktopSize` pseudo-encoding (-308). + +use std::io::{Read, Write}; +use std::os::unix::net::UnixStream; +use std::path::Path; +use std::time::Duration; + +/// RFC 6143 §7.1.1: the version both ends speak. +const VERSION: &[u8; 12] = b"RFB 003.008\n"; +/// §7.1.2: the security type "None". +const SECURITY_NONE: u8 = 1; +/// §7.5.2's `SetEncodings`, and the pseudo-encoding that permits +/// `SetDesktopSize`. +const SET_ENCODINGS: u8 = 2; +const EXTENDED_DESKTOP_SIZE: i32 = -308; +const SET_DESKTOP_SIZE: u8 = 251; + +/// Ask the VNC server at `socket` for a display of `width` x `height`, as a +/// person resizing the host's window does. The connection is answered so the +/// caller can hold it open while the guest follows. +pub fn resize(socket: &Path, width: u16, height: u16) -> Result { + let mut vnc = UnixStream::connect(socket).map_err(|e| format!("vnc: connect {}: {e}", socket.display()))?; + vnc.set_read_timeout(Some(Duration::from_secs(10))).map_err(|e| format!("vnc: {e}"))?; + let io = |e: std::io::Error| format!("vnc: {e}"); + + let mut version = [0u8; 12]; + vnc.read_exact(&mut version).map_err(io)?; + if &version != VERSION { + return Err(format!("vnc: the server speaks {:?}", String::from_utf8_lossy(&version))); + } + vnc.write_all(VERSION).map_err(io)?; + + let mut count = [0u8; 1]; + vnc.read_exact(&mut count).map_err(io)?; + let mut types = vec![0u8; count[0] as usize]; + vnc.read_exact(&mut types).map_err(io)?; + if !types.contains(&SECURITY_NONE) { + return Err(format!("vnc: the server offers security types {types:?}, without None")); + } + vnc.write_all(&[SECURITY_NONE]).map_err(io)?; + let mut result = [0u8; 4]; + vnc.read_exact(&mut result).map_err(io)?; + if result != [0; 4] { + return Err(format!("vnc: the server failed the handshake with {result:?}")); + } + + // §7.3.1 ClientInit, shared; §7.3.2 ServerInit: the size, the pixel + // format and the name, all of which this client leaves unread. + vnc.write_all(&[1]).map_err(io)?; + let mut init = [0u8; 24]; + vnc.read_exact(&mut init).map_err(io)?; + let name = u32::from_be_bytes(init[20..24].try_into().expect("four bytes")) as usize; + vnc.read_exact(&mut vec![0u8; name]).map_err(io)?; + + let mut encodings = vec![SET_ENCODINGS, 0, 0, 1]; + encodings.extend(EXTENDED_DESKTOP_SIZE.to_be_bytes()); + vnc.write_all(&encodings).map_err(io)?; + + // One screen, id 0, at the origin, with no flags. + let mut set = vec![SET_DESKTOP_SIZE, 0]; + set.extend(width.to_be_bytes()); + set.extend(height.to_be_bytes()); + set.extend([1, 0]); + set.extend(0u32.to_be_bytes()); + set.extend([0u8; 4]); + set.extend(width.to_be_bytes()); + set.extend(height.to_be_bytes()); + set.extend(0u32.to_be_bytes()); + vnc.write_all(&set).map_err(io)?; + Ok(vnc) +} diff --git a/tests/guest/metalprobe/src/fb.rs b/tests/guest/metalprobe/src/fb.rs index a21f7f752e7..9c9c79c4aef 100644 --- a/tests/guest/metalprobe/src/fb.rs +++ b/tests/guest/metalprobe/src/fb.rs @@ -61,7 +61,7 @@ impl Scanout { return Err(Refusal::NoScanout); } let bytes = info.stride as usize * info.height as usize * 4; - let shm = SharedMemory::adopt(info.scanout[0], bytes).map_err(|_| Refusal::NoScanout)?; + let shm = SharedMemory::adopt(info.scanout, bytes).map_err(|_| Refusal::NoScanout)?; Ok(Self { _dev: dev, shm, diff --git a/tests/guest/rust-tests/src/bin/abuse_gpu_resolution.rs b/tests/guest/rust-tests/src/bin/abuse_gpu_resolution.rs deleted file mode 100644 index 76471c020a2..00000000000 --- a/tests/guest/rust-tests/src/bin/abuse_gpu_resolution.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! `SYS_GPU_SET_RESOLUTION` must be reachable only through the framebuffer -//! claim. -//! -//! It turns two arbitrary `u32`s into `width * height * 4` bytes of contiguous -//! physical memory, so an ungated caller both reconfigures the display and -//! names a kernel allocation size. -//! -//! **The gate used to be a pid comparison** — "is the caller the process that -//! opened the device?" — and the claim is the argument now, so what this binary -//! presents is the two things a process without one can present: nothing, and -//! a handle that is not a claim. -//! -//! **Both of those now end the caller rather than answering it**, which is why -//! every arm is a child. A handle a process does not hold and a handle of the -//! wrong type are bugs in that process, not conditions to report, so the -//! refusal is `exit 139` and a line in the kernel log — and a test that asserted -//! the old error words would be asserting that the bug is survivable. The child -//! prints its marker before the call, so an arm cannot pass by dying on the way -//! to it. -//! -//! One arm answers instead of killing, and it is here to keep the other four -//! honest: a handle that resolves and carries the wrong *rights* is a question -//! a process is allowed to ask, so it comes back as `PermissionDenied` and this -//! process carries on to the next arm. - -use std::io::Write; -use std::process::{Command, Stdio}; - -use toyos_abi::handle::{Rights, HANDLE_INVALID}; -use toyos_abi::syscall::{self, SyscallError}; -use toyos_abi::{FramebufferInfo, RawHandle}; - -const SELF_PATH: &str = "/system/bin/test_rs_abuse_gpu_resolution"; - -/// `process::HANDLE_FAULT_EXIT_CODE`. -const HANDLE_FAULT: i32 = 139; - -/// `(role, width, height, what the caller presented)`. -/// -/// The two saturating sizes are here because the claim check must fire *ahead* -/// of the driver, and the sane one is here because it must fire *instead* of -/// nothing: an allocation guard that only rejects absurd numbers would pass the -/// first two and let the third through. -const ARMS: &[(&str, u32, u32, &str)] = &[ - // 20000x20000x4 = 1.6 GB of contiguous 2 MiB pages. - ("absent-huge", 20_000, 20_000, "no handle at all, at 1.6 GB"), - // width * height * 4 overflows u32 well before the allocator sees it. - ("absent-saturated", u32::MAX, u32::MAX, "no handle at all, saturated"), - ("absent-sane", 640, 480, "no handle at all, at a resolution the display has"), - ("wrong-type", 640, 480, "a pipe where the call takes a framebuffer claim"), -]; - -fn resize(claim: RawHandle, width: u32, height: u32) -> Result<(), SyscallError> { - let mut info = unsafe { core::mem::zeroed::() }; - unsafe { - syscall::gpu_set_resolution( - claim, - width, - height, - &mut info as *mut FramebufferInfo as *mut u8, - ) - } -} - -fn main() { - match std::env::args().nth(1).as_deref() { - Some(role) => arm(role), - None => test(), - } -} - -fn test() { - for (role, _, _, presented) in ARMS { - let child = Command::new(SELF_PATH) - .arg(role) - .stdout(Stdio::piped()) - .spawn() - .unwrap_or_else(|e| panic!("spawn {role}: {e}")); - let out = child.wait_with_output().unwrap_or_else(|e| panic!("wait {role}: {e}")); - assert_eq!( - String::from_utf8_lossy(&out.stdout).trim(), - format!("reached {role}"), - "{role} ({presented}) never reached the call, or answered past it", - ); - assert_eq!( - out.status.code(), - Some(HANDLE_FAULT), - "{role} ({presented}) did not end the caller", - ); - println!(" {role}: {presented} — killed at the call"); - } - - // A handle that resolves, is of a type the call takes and carries the wrong - // rights. The rights are checked before the type, so this never reaches the - // claim comparison — and it is an answer, because probing what an - // attenuated handle can still do is what attenuation is for. - let blind = syscall::dup_narrowed(RawHandle(1), Rights::NONE) - .expect("a handle carrying nothing is still a handle"); - assert_eq!( - resize(blind, 640, 480), - Err(SyscallError::PermissionDenied), - "a handle with no rights reached the display", - ); - syscall::close(blind); - println!(" rights: refused with a word, and this process is still here"); - - println!("gpu resolution changes are refused without a framebuffer claim"); -} - -fn arm(role: &str) -> ! { - let Some(&(_, width, height, _)) = ARMS.iter().find(|(name, ..)| *name == role) else { - panic!("unknown role {role:?}"); - }; - // Presented before the call and flushed, so "the kernel ended it here" is - // distinguishable from "it never got here". - println!("reached {role}"); - std::io::stdout().flush().expect("flush the marker"); - let claim = if role == "wrong-type" { - // stdout: a handle this process certainly holds, and certainly not a - // claim on the framebuffer. - RawHandle(1) - } else { - HANDLE_INVALID - }; - let answered = resize(claim, width, height); - panic!("{role} was answered {answered:?} instead of ending the caller"); -} diff --git a/tests/images/clipboardcase.toml b/tests/images/clipboardcase.toml index d4db604830d..f52bbb3be7a 100644 --- a/tests/images/clipboardcase.toml +++ b/tests/images/clipboardcase.toml @@ -1,7 +1,8 @@ -# The boot `the_opening_exchange_with_qemus_agent_completes` reads: the -# clipboard agent against QEMU's own vdagent, with the compositor whose -# clipboard it watches, on `Profile::HeadlessVirtioGpu`, the one virtio machine -# with a framebuffer for the compositor to claim. +# The boot `the_opening_exchange_with_qemus_agent_completes` and +# `the_compositor_drives_the_virtio_gpu` read: the clipboard agent against +# QEMU's own vdagent, with the compositor whose clipboard it watches, on +# `Profile::HeadlessVirtioGpu`, the one virtio machine with a display for the +# compositor to drive. # # test-runner makes the boot announce itself. @@ -18,7 +19,7 @@ syscap = ["logread"] [programs.compositor] service = true serves = ["compositor", "prompt", "clipboard", "input"] -devices = ["framebuffer", "keyboard", "mouse"] +devices = ["pci:1af4:1050", "keyboard", "mouse"] [programs.vdagent] devices = ["pci:1af4:1043"] diff --git a/tests/toyos.rs b/tests/toyos.rs index 86f930e33cd..d6cfc2ba202 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -445,6 +445,15 @@ const MACHINE_TESTS: &[&str] = &[ // end of the handshake and of the request is QEMU's vdagent chardev and // virtio-serial device, which no host test runs. "the_opening_exchange_with_qemus_agent_completes", + // The compositor driving QEMU's virtio-gpu through its claim, and a host + // display resize reaching the guest's mode: no metal machine has a + // virtio-gpu, and the host tests drive `toyos-virtio-gpu` against a model + // written from this design's own reading of §5.7, so only QEMU's device + // says it was read right. x86-64 q35 alone, `Profile::HeadlessVirtioGpu`: + // an AArch64 `virt` boot hands over no PCI claim yet. A boot of its own, + // since its VNC display gives QEMU's clipboard a peer the clipboard + // test's boot must not have. + "the_compositor_drives_the_virtio_gpu", ]; /// **The metal profile**: which registrations run on the ThinkPad T14, what @@ -3706,7 +3715,7 @@ fn scanout_wc(console: &str) -> Result<(), String> { /// test. fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { match name { - "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(test_config), + "iommu_virtio_platform" => common::iommu::iommu_virtio_platform(), "amdvi_firmware_left" => common::iommu::amdvi_firmware_left(test_config), "amdvi_selftest" => common::iommu::amdvi_selftest(test_config), "nested_nmi_is_loud" => faults::nested_nmi_is_loud(test_config), @@ -3728,6 +3737,7 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "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(), + "the_compositor_drives_the_virtio_gpu" => the_compositor_drives_the_virtio_gpu(), other => Err(format!("unknown machine test {other}")), } } @@ -3756,6 +3766,55 @@ fn the_opening_exchange_with_qemus_agent_completes() -> Result<(), String> { } } +/// The compositor drives QEMU's virtio-gpu through its `pci:1af4:1050` +/// claim: it starts at the size the device prefers, QEMU's default +/// 1280x800, and its first frame reaches the scanout; then a VNC client +/// resizes the host's display to 1024x768, the device raises its display +/// event, and the compositor flips to that mode and draws it. +/// +/// "Drawn" is a screendump of the scanout at the mode's size holding the +/// wallpaper's many colours: a scanout no frame reached is one colour, or +/// QEMU's two-colour notice that the guest set none. +fn the_compositor_drives_the_virtio_gpu() -> Result<(), String> { + const FIRST: &str = "compositor: display 1280x800"; + const FRAME: &str = "compositor: the first frame is on the panel"; + const FOLLOWED: &str = "compositor: mode 1024x768"; + let mut qemu = QemuInstance::boot_with_options( + &compile::repo_root().join("tests/images/clipboardcase.toml"), + &[], + &[], + BootOptions { profile: qemu::Profile::HeadlessVirtioGpu, qmp: true, vnc: true, ..Default::default() }, + ); + let mut console = format!("{}\n", qemu.boot_log()); + await_guest(&mut qemu, &mut console, "the compositor's first frame", |t| t.contains(FRAME)) + .map_err(|e| format!("{e}\n{console}"))?; + let drawn = |dump: &screen::Ppm, (width, height)| -> Result<(), String> { + let colours: BTreeSet<[u8; 3]> = dump.pixels.iter().copied().collect(); + if (dump.width, dump.height) != (width, height) || colours.len() < 64 { + return Err(format!( + "the scanout is {}x{} in {} colour(s), where the desktop at {width}x{height} was drawn", + dump.width, + dump.height, + colours.len() + )); + } + Ok(()) + }; + drawn(&qemu.screendump(), (1280, 800)).map_err(|e| format!("{e}\n{console}"))?; + + let _client = common::vnc::resize(qemu.vnc_socket(), 1024, 768)?; + await_guest(&mut qemu, &mut console, "the compositor following the host's display", |t| t.contains(FOLLOWED)) + .map_err(|e| format!("{e}\n{console}"))?; + // The flip's frame is drawn into the new scanout before it is shown. + drawn(&qemu.screendump(), (1024, 768)).map_err(|e| format!("{e}\n{console}"))?; + + let said = serial::Serial::named("the virtio-gpu boot", console); + said.must_be_clean()?; + said.must_say(FIRST)?; + eprintln!(" [virtio-gpu] 1280x800 drawn, then 1024x768 followed and drawn"); + Ok(()) +} + /// A machine with one NVMe disk and no USB controller boots the image off /// that disk to ready, and says nothing a clean boot does not. fn nvme_disk_boots(test_config: &Path) -> Result<(), String> { From 1ab1d9917088c4bfeab285242c93b30914263d26 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 14 Oct 2026 18:46:32 +0200 Subject: [PATCH 2/2] Review: the vacuous mint control goes, and the lend window's first-fit refusal is said - Every claim description installs one handle now, so the mint-rollback self-test could not go red for the defect it was written for: it goes with its actuator, its judge, its metal row, `install_all`, `stage_room` and `Region::empty`; a description is one `install`. - `toyos-desktop`'s fit assertion says that the window holds two of the largest scanouts only together: first-fit placement can leave no run for a mode past half of it, a flip declined with the old mode kept. `issues/a-lent-scanout-is-one-contiguous-run.md` records the sequence. - `InitRefusal`'s doc names `offer` and `accept`. - `issues/a-refused-framebuffer-reads-as-no-display.md` says what the virtio-gpu boot measured: the kernel's line is the same for both shapes. - The compositor ends on a virtio-gpu function it was given and cannot bring up rather than falling back; `HeadlessVirtioGpu` states the device's preferred size; the panic console's `detach`/`rearm` are private; the restarted-holder issue names its owner. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016UzbDnM9m6nQWhbjGi6Lv1 --- .../a-lent-scanout-is-one-contiguous-run.md | 15 +++++- ...refused-framebuffer-reads-as-no-display.md | 16 +++--- ...-lends-around-its-predecessors-scanouts.md | 3 +- kernel/src/actuator.rs | 2 - kernel/src/drivers/panic_console/mod.rs | 9 ++-- kernel/src/drivers/virtio.rs | 3 +- kernel/src/leak_selftest.rs | 7 --- kernel/src/main.rs | 5 -- kernel/src/object/device.rs | 51 ++++--------------- kernel/src/object/handle.rs | 29 ----------- kernel/src/object/shm.rs | 9 ---- system/compositor/desktop/src/mode.rs | 13 ++--- system/compositor/src/display.rs | 16 +++--- tests/common/qemu.rs | 4 +- tests/metal/src/lib.rs | 1 - tests/toyos.rs | 24 ++------- 16 files changed, 56 insertions(+), 151 deletions(-) delete mode 100644 kernel/src/leak_selftest.rs diff --git a/issues/a-lent-scanout-is-one-contiguous-run.md b/issues/a-lent-scanout-is-one-contiguous-run.md index dba0a71f76a..cb2e9deb665 100644 --- a/issues/a-lent-scanout-is-one-contiguous-run.md +++ b/issues/a-lent-scanout-is-one-contiguous-run.md @@ -12,10 +12,21 @@ makes is one physically contiguous run of 2 MiB pages (`kernel/src/object/shm.rs`, `SharedMemObject::create`). A mode at the largest bound is 64 such pages; on a machine whose memory is fragmented there may be no run that long, and the mode change is then declined by name and the -old mode stays, though the memory exists. Virtio 1.2 §5.7.6.1 already takes a +old mode stays, though the memory exists. + +The window a claim lends in is placed first-fit too (`kernel/src/pcidev/mod.rs`, +`Bound::lend_room`). `toyos-desktop` sizes `LEND_WINDOW` for two scanouts at +the largest mode, which holds only while nothing splits it: boot at 1280x800 +(2 MiB pages 0-1), flip to 7680x4320 (pages 2-65, freeing 0-1), then ask for +7680x4300 (63 pages) — the free runs are 2 and 62 pages and the flip is +declined by name, the old mode kept. Only modes past about half the window +reach it. + +Virtio 1.2 §5.7.6.1 already takes a scatter list ("the framebuffer doesn't need to be contignous in guest physical memory"), and `RESOURCE_ATTACH_BACKING` takes one entry per run. Owner: the memory track. Exit: a region backed by more than one run, lent as one mapping in the function's domain or as one entry per run, and a mode -change on a machine with no run of its length served. +change on a machine with no run of its length served; and a lend window whose +free addresses serve any lend no larger than their total. diff --git a/issues/a-refused-framebuffer-reads-as-no-display.md b/issues/a-refused-framebuffer-reads-as-no-display.md index 6e48e2b8eca..195e67e41bb 100644 --- a/issues/a-refused-framebuffer-reads-as-no-display.md +++ b/issues/a-refused-framebuffer-reads-as-no-display.md @@ -30,14 +30,14 @@ the bootloader's line above the second — printed by `query_gop` metal-sim boot with `mute: true` and the T14 itself do not have. Nothing reaches the kernel, so nothing reaches `/log`, the panel, or a crash report. -`main.rs:520` then branches on `kernel_args.gop_framebuffer != 0` and takes the -same arm for both. On this harness the second shape is rescued by the -compositor's `pci:1af4:1050` claim on the same device. On a -machine whose Blt-only display has no kernel driver — which the T14 is until an -Intel display engine driver exists — the branch falls to `main.rs:529` and both -shapes print `GPU: the firmware handed over no framebuffer`. That inference is from the -branch, not measured: QEMU cannot stage a Blt-only GOP on a device this kernel -has no driver for. +`kernel/src/main.rs` then branches on `kernel_args.gop_framebuffer != 0` and +takes the same arm for both: both print `GPU: the firmware handed over no +framebuffer`, measured for the Blt-only shape on `Profile::HeadlessVirtioGpu` +(`the_compositor_drives_the_virtio_gpu`'s boot log). There the compositor's +`pci:1af4:1050` claim draws the desktop, but nothing in the kernel's record +tells the two shapes apart, and on a machine whose Blt-only display no +process drives — the T14, until an Intel display engine driver exists — the +record is all there is. The distinction is the difference between "this machine has no panel" and "this machine has a panel and something upstream refused it", which are different diff --git a/issues/a-restarted-display-holder-lends-around-its-predecessors-scanouts.md b/issues/a-restarted-display-holder-lends-around-its-predecessors-scanouts.md index 4c805a19bfa..79f44c83bc7 100644 --- a/issues/a-restarted-display-holder-lends-around-its-predecessors-scanouts.md +++ b/issues/a-restarted-display-holder-lends-around-its-predecessors-scanouts.md @@ -21,6 +21,7 @@ included, until the next holder's own reset (§3.1.1, which The supervisor does not restart the compositor, so nothing reaches this today. -Exit: a residue a holder's own §3.1.1 reset retires — the function's status +Owner: pcidev, on the small-kernel track. Exit: a residue a holder's own +§3.1.1 reset retires — the function's status read 0 after the reset is the proof it holds nothing — so the window is whole again for the next holder. diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index df27655f356..021f9b10bf8 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -201,8 +201,6 @@ actuators! { /// Cap how long an idle CPU may sleep so the idle loop keeps running. diag_tick = "diag-tick"; - /// Run the leak-rollback controls (device mint) after mount. - leak_rollback_selftest = "leak-rollback-selftest"; /// Run the revoked-backing controls after mount. revoked_backing_selftest = "revoked-backing-selftest"; diff --git a/kernel/src/drivers/panic_console/mod.rs b/kernel/src/drivers/panic_console/mod.rs index da7cac24beb..3ca4803f4a3 100644 --- a/kernel/src/drivers/panic_console/mod.rs +++ b/kernel/src/drivers/panic_console/mod.rs @@ -347,14 +347,13 @@ fn publish(fb: Fb) { FB.publish(fb.words()); } -/// Stop painting until the next [`rearm`]: no console, never a write into -/// memory the scanout no longer is. -pub fn detach() { +/// Stop painting: no console, never a write into memory that is no scanout. +fn detach() { publish(Fb::DETACHED); } -/// Re-publish the armed descriptor. -pub fn rearm() { +/// Publish the armed descriptor. +fn rearm() { // SAFETY: `PENDING`'s one writer is `arm`, single-threaded at boot; `Fb` // is `Copy`, so this holds no reference into the cell. let fb = unsafe { *PENDING.0.get() }; diff --git a/kernel/src/drivers/virtio.rs b/kernel/src/drivers/virtio.rs index 7b7fa3def50..f8dee90b0e0 100644 --- a/kernel/src/drivers/virtio.rs +++ b/kernel/src/drivers/virtio.rs @@ -118,7 +118,8 @@ impl core::fmt::Display for MissingCap { } } -/// Why [`VirtioDevice::init`] refused the device; either way it keeps no DMA. +/// Why [`VirtioDevice::offer`] or [`Offered::accept`] refused the device; +/// either way it keeps no DMA. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum InitRefusal { /// Its chain never produced a window for a required capability. diff --git a/kernel/src/leak_selftest.rs b/kernel/src/leak_selftest.rs deleted file mode 100644 index b9db9326c57..00000000000 --- a/kernel/src/leak_selftest.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Negative controls for the leak-rollback fixes: each reproduces an "acquire -//! before a fallible step" site and asserts the in-tree count returns to baseline. Run behind `leak-rollback-selftest`. - -/// Runs every leak-rollback control. -pub fn run() { - crate::object::device::mint_rollback_selftest(); -} diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 240246f1ae6..9da33092c47 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -53,7 +53,6 @@ mod rollback; mod rootfs; mod file_cache; #[cfg(feature = "boot-actuators")] -mod leak_selftest; #[cfg(feature = "boot-actuators")] mod revoke_selftest; mod tmpfs; @@ -506,10 +505,6 @@ pub(crate) unsafe extern "C" fn kernel_main(loader_args: &mut KernelArgs) -> ! { gpt::probe_usb_disks(); rootfs::hold_source(); - #[cfg(feature = "boot-actuators")] - if actuator::leak_rollback_selftest() { - leak_selftest::run(); - } #[cfg(feature = "boot-actuators")] if actuator::revoked_backing_selftest() { revoke_selftest::run(); diff --git a/kernel/src/object/device.rs b/kernel/src/object/device.rs index 841e3855cd9..fc235dc8cc1 100644 --- a/kernel/src/object/device.rs +++ b/kernel/src/object/device.rs @@ -11,7 +11,7 @@ use toyos_abi::FramebufferInfo; use crate::device::{Claim, DeviceType}; -use super::handle::HandleTable; +use super::handle::{HandleEntry, HandleTable}; use super::shm::SharedMemObject; use super::{Held, KObjectRef, KObjectVariant, ObjectCore, ZeroHandles}; @@ -37,27 +37,22 @@ pub enum DeviceInfo { // MAP is required; DUP and TRANSFER let a daemon hand the buffer to another process. const BUFFER_RIGHTS: Rights = Rights::MAP.union(Rights::DUP).union(Rights::TRANSFER); -fn install_buffers( - table: &mut HandleTable, - buffers: &[&Arc], -) -> Result, SyscallError> { - let objects = buffers - .iter() - .map(|b| (KObjectRef::SharedMem((*b).clone()), BUFFER_RIGHTS)) - .collect(); - table.install_all(objects).map_err(|_| SyscallError::ResourceExhausted) +fn install_buffer(table: &mut HandleTable, buffer: &Arc) -> Result { + table + .install(HandleEntry::new(KObjectRef::SharedMem(Arc::clone(buffer)), BUFFER_RIGHTS)) + .map_err(|_| SyscallError::ResourceExhausted) } impl DeviceInfo { - /// The description as bytes, with a handle installed for every named buffer. - // All-or-nothing: a table too full for the whole batch installs none and leaves - // `described.bytes` unset, so the next read re-mints instead of binding stranded handles. + /// The description as bytes, with a handle installed for the one buffer it + /// names. A table with no room leaves `described.bytes` unset, so the next + /// 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_buffers(table, &[scanout])?[0]; + info.scanout = install_buffer(table, scanout)?; bytes(&info).into() } // Nothing to install: every address in it is a size, and the memory @@ -68,7 +63,7 @@ impl DeviceInfo { Self::Acpi(info) => bytes(info).into(), Self::Hda(info, pcm) => { let mut info = *info; - info.pcm = install_buffers(table, &[pcm])?[0]; + info.pcm = install_buffer(table, pcm)?; bytes(&info).into() } }) @@ -183,32 +178,6 @@ impl DeviceClaim { } } -/// Negative control for the mint's all-or-nothing rule: a framebuffer -/// description minted into a table with no room installs nothing. -#[cfg(feature = "boot-actuators")] -pub(crate) fn mint_rollback_selftest() { - use crate::object::shm::{Region, SharedMemObject}; - let mut table = HandleTable::new(); - table.stage_room(0); - let info = DeviceInfo::Framebuffer( - FramebufferInfo { - scanout: toyos_abi::HANDLE_INVALID, - width: 0, - height: 0, - stride: 0, - pixel_format: 0, - edid: [0; toyos_abi::boot::EDID_BLOCK], - edid_len: 0, - }, - SharedMemObject::over(Region::empty()), - ); - let before = table.iter().count(); - let refused = info.mint(&mut table).is_err(); - let installed = table.iter().count() - before; - let verdict = if refused && installed == 0 { "PASS" } else { "FAIL" }; - crate::log!("leak-selftest: device-mint {verdict} (refused={refused} installed={installed})"); -} - // Released on last handle, not last Arc: a parked daemon can strand an Arc without releasing. impl ZeroHandles for DeviceClaim { fn on_zero_handles(&self) { diff --git a/kernel/src/object/handle.rs b/kernel/src/object/handle.rs index b7f7ea11327..248cd5802b3 100644 --- a/kernel/src/object/handle.rs +++ b/kernel/src/object/handle.rs @@ -197,26 +197,6 @@ impl HandleTable { Ok(RawHandle::new(slot, 0)) } - /// Install every object all-or-nothing: a batch that would overflow the - /// table is refused whole, leaving the table exactly as it was found. - // `has_room` reserves the slots before any `HandleEntry::new`, so a refusal - // builds and installs nothing — undoing a partial install would retire the objects. - pub fn install_all( - &mut self, - objects: Vec<(KObjectRef, Rights)>, - ) -> Result, TableFull> { - if !self.has_room(objects.len()) { - return Err(TableFull); - } - Ok(objects - .into_iter() - .map(|(object, rights)| { - self.install(HandleEntry::new(object, rights)) - .expect("has_room reserved every slot") - }) - .collect()) - } - /// Install at a caller-chosen slot, replacing whatever is there. #[must_use = "the displaced entry must be dropped by the caller"] pub fn install_at( @@ -421,15 +401,6 @@ impl HandleTable { }) } - /// Self-test: fill the table to `room` slots of capacity with empty, - // non-free slots `install` can neither reuse nor grow past. - #[cfg(feature = "boot-actuators")] - pub fn stage_room(&mut self, room: usize) { - while self.slots.len() < MAX_HANDLES - room { - self.slots.push(Slot::Serving { generation: 0, entry: None }); - } - } - /// Test actuator: park a free slot at its last generation and answer the handle its next install will carry. // Real exhaustion needs a generation's worth of close/install cycles; this stages the field directly. #[cfg(feature = "test-actuators")] diff --git a/kernel/src/object/shm.rs b/kernel/src/object/shm.rs index 8e935f49dee..0259dcb905d 100644 --- a/kernel/src/object/shm.rs +++ b/kernel/src/object/shm.rs @@ -41,15 +41,6 @@ pub struct Region { pub pages: Option>, } -impl Region { - /// Size zero, which maps nothing: a description's buffer where a control - /// needs one and no pages. - #[cfg(feature = "boot-actuators")] - pub fn empty() -> Self { - Self { phys: DirectMap::from_phys(0), size: 0, cache: CachePolicy::Normal, pages: None } - } -} - pub struct SharedMemObject { pub(super) core: ObjectCore, region: Region, diff --git a/system/compositor/desktop/src/mode.rs b/system/compositor/desktop/src/mode.rs index 72dc3d65b9c..543223e4c23 100644 --- a/system/compositor/desktop/src/mode.rs +++ b/system/compositor/desktop/src/mode.rs @@ -54,7 +54,11 @@ pub const LEAST: Mode = { /// engine (Intel ARK, Tiger Lake GT2 `8086:9A49`: DisplayPort 7680x4320). pub const LARGEST: Mode = Mode::new(7680, 4320); -// A mode change lends the new scanout before it gives the old one back. +// A mode change lends the new scanout before it gives the old one back, so the +// window holds two of the largest. It holds them only together: pcidev places +// a lend first-fit, so a small scanout left where it split the window can leave +// no run for a mode past half of it, and that flip is declined with the old +// mode kept and said (`issues/a-lent-scanout-is-one-contiguous-run.md`). const _: () = assert!(2 * LARGEST.scanout_bytes() <= LEND_WINDOW); /// The mode a display starts in, from the size its device prefers. @@ -130,13 +134,6 @@ mod tests { assert_eq!(LEAST, Mode::new(642, 446)); } - /// Two scanouts at the largest mode fill the lend window exactly. - #[test] - fn two_of_the_largest_scanouts_fill_the_lend_window() { - assert_eq!(LARGEST.scanout_bytes(), 64 * SHM_PAGE as u64); - assert_eq!(2 * LARGEST.scanout_bytes(), LEND_WINDOW); - } - #[test] fn a_preferred_size_inside_the_bounds_is_the_mode() { assert_eq!(Start::of(Some(Mode::new(1280, 800))), Start::Preferred(Mode::new(1280, 800))); diff --git a/system/compositor/src/display.rs b/system/compositor/src/display.rs index 7335a0ea17c..9be298cb5af 100644 --- a/system/compositor/src/display.rs +++ b/system/compositor/src/display.rs @@ -52,16 +52,14 @@ pub enum Display { impl Display { /// The display this machine gives the compositor; ends it where there is - /// none, since a desktop with no panel has nothing to draw on. + /// none, since a desktop with no panel has nothing to draw on, and where + /// the virtio-gpu function it was given cannot be brought up. pub fn claim() -> (Self, Scale) { if let Some(dev) = endow::pci_function::(PCI_ID) { - match Virtio::open(dev) { - Ok(virtio) => { - let scale = virtio.scale; - return (Self::Virtio(virtio), scale); - } - Err(why) => eprintln!("compositor: the virtio-gpu function cannot show the desktop: {why}"), - } + let virtio = Virtio::open(dev) + .unwrap_or_else(|why| panic!("compositor: the virtio-gpu function cannot show the desktop: {why}")); + let scale = virtio.scale; + return (Self::Virtio(virtio), scale); } let claim: FramebufferDev = endow::device(DeviceType::Framebuffer) .expect("compositor: this machine gives it neither a virtio-gpu function nor a framebuffer"); @@ -291,8 +289,6 @@ impl Virtio { Grant::alloc(&dev, toyos_virtio_gpu::GRANT_BYTES as u64).map_err(|why| why.to_string())?; grant.window().zero(); let queues = Queues::new(grant); - // `num_scanouts` read, so the structure is known to hold the two - // event fields before it. let (setup, scanouts) = setup .config_vector(MSIX_ENTRY) .and_then(|s| s.enable(&queues.control, MSIX_ENTRY)) diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 3a5b31d1145..b4f26619d82 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -2306,7 +2306,9 @@ fn qemu_command( qemu.arg("-device").arg("virtio-rng-pci"); } if shape.virtio_gpu { - qemu.arg("-device").arg(format!("virtio-gpu-pci{platform}")); + // The size the device prefers at start, stated rather than QEMU's + // default: `the_compositor_drives_the_virtio_gpu` reads it back. + qemu.arg("-device").arg(format!("virtio-gpu-pci,xres=1280,yres=800{platform}")); } if shape.probes == Probes::Testdevs { // Two, the first enumerated below the second: the SMMU's selftest diff --git a/tests/metal/src/lib.rs b/tests/metal/src/lib.rs index 73973476c84..b537770ba69 100644 --- a/tests/metal/src/lib.rs +++ b/tests/metal/src/lib.rs @@ -747,7 +747,6 @@ pub const FLASHABLE: &[&str] = &[ // an armed image leaves behind is a longer log. "pci-cap-selftest", "revoked-backing-selftest", - "leak-rollback-selftest", "lapic-spurious-selftest", "unclaimed-vector-selftest", "xhci-xecp-selftest", diff --git a/tests/toyos.rs b/tests/toyos.rs index d6cfc2ba202..b94535715ec 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -450,9 +450,9 @@ const MACHINE_TESTS: &[&str] = &[ // virtio-gpu, and the host tests drive `toyos-virtio-gpu` against a model // written from this design's own reading of §5.7, so only QEMU's device // says it was read right. x86-64 q35 alone, `Profile::HeadlessVirtioGpu`: - // an AArch64 `virt` boot hands over no PCI claim yet. A boot of its own, - // since its VNC display gives QEMU's clipboard a peer the clipboard - // test's boot must not have. + // an AArch64 `virt` boot hands over no PCI claim yet. A boot of its own: + // its `-vnc` display changes the display configuration the clipboard + // test's premise, `-display none`, rests on. "the_compositor_drives_the_virtio_gpu", ]; @@ -985,10 +985,6 @@ const METAL: &[(&str, metal::Metal)] = &[ "read_fault_selftests", metal::Metal { arms: SELFTESTS, judge: |b| read_fault_probes(b[0].kernel().text()) }, ), - ( - "leak_rollback_selftest", - metal::Metal { arms: SELFTESTS, judge: |b| leak_rollback(b[0].kernel().text()) }, - ), ( "lapic_spurious_vector", metal::Metal { arms: SELFTESTS, judge: |b| lapic_vectors(b[0].kernel().text()) }, @@ -1270,7 +1266,6 @@ const PROCTREECASE: &[metal::Arm] = &[metal::once( const SELFTEST_PARAMS: &[&str] = &[ "pci-cap-selftest", "revoked-backing-selftest", - "leak-rollback-selftest", "lapic-spurious-selftest", "unclaimed-vector-selftest", "xhci-xecp-selftest", @@ -4339,19 +4334,6 @@ fn read_fault_probes(log: &str) -> Result<(), String> { Ok(()) } -/// An "acquire before a fallible step" control: the count returned to its baseline after a refused call. -fn leak_rollback(log: &str) -> Result<(), String> { - let probe = "leak-selftest: device-mint"; - let Some(verdict) = log.lines().find(|l| l.contains(probe)) else { - return Err(format!("{probe} never ran:\n{log}")); - }; - if !verdict.contains("PASS") { - return Err(format!("{}\n{log}", verdict.trim())); - } - eprintln!(" [leak] {}", verdict.trim()); - Ok(()) -} - /// The spurious vector and an unclaimed one are both gated rather than escalated to #DF. fn lapic_vectors(log: &str) -> Result<(), String> { for kind in ["spurious", "unclaimed"] {