diff --git a/Cargo.lock b/Cargo.lock index 7e1db4c63a6..1cefe527119 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -579,6 +579,9 @@ dependencies = [ "toyos-input", "toyos-inspect", "toyos-manifest", + "toyos-pci-claim", + "toyos-virtio", + "toyos-virtio-gpu", "toyos-window", ] @@ -4878,6 +4881,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 3abd160ffe6..ee8c5ddfd59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,6 +95,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 9fae23d7246..603aaab27bf 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:03:80:00/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..cb2e9deb665 --- /dev/null +++ b/issues/a-lent-scanout-is-one-contiguous-run.md @@ -0,0 +1,32 @@ +--- +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. + +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; 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 dcd7322f657..80a5838a0b0 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 -`virtio_gpu::init` claiming the same device and logging `GPU: using VirtIO`. 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 -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:03:80:00/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 new file mode 100644 index 00000000000..79f44c83bc7 --- /dev/null +++ b/issues/a-restarted-display-holder-lends-around-its-predecessors-scanouts.md @@ -0,0 +1,27 @@ +--- +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. + +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/issues/every-driver-is-still-in-the-kernel.md b/issues/every-driver-is-still-in-the-kernel.md index 9c5cfc35ee5..a2d8deb733a 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:03:80:00/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..64aca150977 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:03:80:00/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..021f9b10bf8 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 @@ -204,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/device.rs b/kernel/src/device.rs index 33358c1ad07..bd47ebb5c6e 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 { @@ -286,11 +285,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..3ca4803f4a3 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,27 @@ 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. -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 after such a window. -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. +/// 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() }; 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 +444,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 +490,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..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. @@ -159,18 +160,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 +682,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 +728,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 +793,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/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 c44c39c2b65..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; @@ -88,7 +87,6 @@ mod pipe; mod device; mod pcidev; mod isa; -mod gpu; mod user_ptr; mod vma; mod syscall; @@ -108,7 +106,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 +177,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. @@ -511,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(); @@ -535,12 +525,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 +536,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..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}; @@ -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,41 +34,25 @@ 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); -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, 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_buffer(table, scanout)?; bytes(&info).into() } // Nothing to install: every address in it is a size, and the memory @@ -78,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() } }) @@ -98,7 +83,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,51 +176,6 @@ 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. -#[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()), - }; - let info = DeviceInfo::Framebuffer( - FramebufferInfo { - scanout: [toyos_abi::HANDLE_INVALID; 2], - cursor: 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, - ); - 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. 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 0f58e678174..0259dcb905d 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)] @@ -47,14 +41,6 @@ pub struct Region { pub pages: Option>, } -impl Region { - /// A placeholder for a driver struct built before its buffers exist: - /// size zero maps nothing. - 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/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index c0c5fe0f750..1f4486a75a9 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 @@ -125,7 +125,7 @@ use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering}; use record::Interrupt; use toyos_abi::boot::{MemoryMapEntry, RootBridgeWindow}; use toyos_abi::inventory::Arrival; -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; @@ -147,8 +147,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. @@ -230,7 +230,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, } @@ -319,9 +319,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)) } @@ -1031,7 +1031,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 }) } @@ -1660,7 +1660,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); @@ -1734,9 +1739,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| { @@ -1747,10 +1751,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 6612e2ae3be..40082bd954a 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 3a41a78b399..aa522196a4e 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 @@ -419,24 +404,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) @@ -701,8 +668,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 e590afc7933..47746e7daa1 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`]. /// @@ -111,7 +108,6 @@ pub const SYS_CLOCK_EPOCH: u64 = 75; 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 @@ -1203,54 +1199,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). /// @@ -2446,10 +2394,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 8b4e04da531..5ff5c4ead79 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..543223e4c23 --- /dev/null +++ b/system/compositor/desktop/src/mode.rs @@ -0,0 +1,173 @@ +//! 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, 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. +#[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)); + } + + #[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..eda561506b4 --- /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; + +/// A display controller of another kind, class `03:80:00`, of virtio's vendor +/// id and the GPU device's (§4.1.2.1: `0x1040` + 16), as the manifest row +/// spells the claim. +const FUNCTION: toyos_manifest::device::PciMatch = toyos_manifest::device::PciMatch::new( + [0x03, 0x80, 0x00], + Some(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, and where + /// the virtio-gpu function it was given cannot be brought up. + pub fn claim() -> (Self, Scale) { + if let Some(dev) = endow::claim::(&FUNCTION.to_string()) { + 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"); + 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); + 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 77dff741106..0d3208a996b 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 41355514685..5cca582e1e3 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 @@ -934,7 +934,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, } @@ -1085,6 +1085,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`] @@ -1149,6 +1152,7 @@ impl Default for BootOptions { smp: 2, profile: Profile::Headless, qmp: false, + vnc: false, kernel_features: &[], kernel_params: &[], mute: false, @@ -1661,6 +1665,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"); @@ -2246,6 +2254,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}")); } @@ -2304,7 +2315,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 @@ -2401,13 +2414,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 } } } @@ -2416,6 +2431,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 3976ac1c957..a4964678451 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:03:80:00/1af4:1050", "keyboard", "mouse"] # One process per virtio-serial function the kernel does not drive: the # clipboard's channel, beside the kernel's console. 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 748bf0beeda..f2677f7c62a 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -455,6 +455,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: + // its `-vnc` display changes the display configuration the clipboard + // test's premise, `-display none`, rests on. + "the_compositor_drives_the_virtio_gpu", ]; /// **The metal profile**: which registrations run on the ThinkPad T14, what @@ -986,10 +995,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()) }, @@ -1271,7 +1276,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", @@ -3716,7 +3720,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), @@ -3739,6 +3743,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}")), } } @@ -3767,6 +3772,55 @@ fn the_opening_exchange_with_qemus_agent_completes() -> Result<(), String> { } } +/// The compositor drives QEMU's virtio-gpu through its `pci:03:80:00/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> { @@ -4332,19 +4386,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"] {