Skip to content

Design: a display scale factor and live mode changes, on QEMU and metal - #895

Draft
Japabu wants to merge 1 commit into
mainfrom
wt/toyos-hidpi
Draft

Japabu wants to merge 1 commit into
mainfrom
wt/toyos-hidpi

Conversation

@Japabu

@Japabu Japabu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Design round. Nothing is built. The owner's go: a display scale factor (pixel density) and live resize / resolution change, on QEMU and on metal alike. Each stage below is its own pull request, built only once this design is accepted in a comment here. The branch carries only the empty design commit, which the build drops. Production and test lines: 0.

What this was read from:

  • main at ab655a115.
  • QEMU v11.1.1 (c3d48b7), edk2 05b96e3, virtio 1.2 cs01, wayland 93c04ad3, wayland-protocols 35d9638c, Mutter 65fc6baa, and the winit fork's toyos-0.30.13 at f58e1f3b (the commit Cargo.lock consumes). Every file and line is in sources.txt.

What was measured:

What the tree has

  • There is no userland GPU driver. The kernel drives both displays: kernel/src/drivers/virtio_gpu.rs and kernel/src/drivers/gop.rs. They sit behind the Gpu trait (kernel/src/gpu.rs) and four syscalls: SYS_GPU_PRESENT, _SET_CURSOR, _MOVE_CURSOR and _SET_RESOLUTION. The compositor holds the framebuffer claim and calls them (sdk/toyos/src/gpu.rs). Moving virtio-gpu out is stage 1 of issues/every-driver-is-still-in-the-kernel.md.
  • A mode change exists, but only on request.
    • screen WxH (toybox) sends MSG_SET_RESOLUTION, which calls SYS_GPU_SET_RESOLUTION. The kernel makes a new resource and scanout, and the compositor reflows its windows (system/compositor/src/session.rs:1155).
    • GOP answers NotSupported (gop.rs:21), and the compositor drops that without a word.
    • Nothing reads VIRTIO_GPU_EVENT_DISPLAY. The kernel driver spin-polls its used ring (drivers/virtio.rs:534) and takes no config interrupt.
  • The boot mode comes from EDID's first descriptor, floored at 1280x720 (virtio_gpu.rs:592-606).
    • GET_DISPLAY_INFO is never sent.
    • The panel's physical size is never read.
    • On a large-screen EDID (measured below), the bytes it reads are not a timing.
  • Nothing has a scale.
    • Chrome is Chrome::DEFAULT, in pixels (system/compositor/desktop/src/layout.rs:83).
    • Text is one 8x16 raster made by the build (build/assets.rs:301).
    • The build draws each icon at one size (build/icons.rs:27).
    • The window protocol carries pixels only (sdk/window/src/lib.rs:243,279,292).
    • The winit fork answers scale_factor() with 1.0, and takes a requested size as physical at 1.0 (src/platform_impl/toyos/window.rs:31-34,86-89).
  • The surface API.
    • sdk/toyos/src/surface.rs carries key transitions down the surface tree and no pixels. Nothing in it changes.
    • The pixel surface apps use is toyos-window's Window (sdk/window), with winit's ToyOS backend over it.
    • terminal, editor, files, paint and filepicker draw through Window. calc, snake and doom draw through winit and softbuffer.
  • A display function in userland does not fit today's DMA bounds.
    • A claim holds at most 32 MiB of grants and lends together, and one grant is at most 8 MiB (kernel/src/pcidev/mod.rs:152,156).
    • A mode switch holds two scanouts. Two at 2560x1600 fill exactly 32 MiB in 2 MiB spans, leaving no room for the rings. Two at 3456x2234 take 62 MB.
    • MAX_FUNCTIONS is 4 (:146). The Virtio profile's three claims (net, sound, NVMe) plus the GPU take all four.

What QEMU does

Read from the QEMU 11.1.1 source:

  • cocoa reports the window in backing pixels and in millimetres. On a window resize, updateUIInfoLocked sends two things through qemu_console_set_ui_info, debounced by 1 s:

    • the size: the frame times backingScaleFactor;
    • width_mm/height_mm: the frame's share of CGDisplayScreenSize.

    So on a Retina panel the guest is offered the window's real pixels and the panel's real density.

  • virtio-gpu turns that into the spec's event. virtio_gpu_ui_info stores the report as the scanout's preferred size and mm. It sets VIRTIO_GPU_EVENT_DISPLAY in events_read and sends a config notification.

    • GET_DISPLAY_INFO answers that size.
    • GET_EDID generates an EDID from it, at 100 dpi when no mm is known.
    • Until the UI reports, the size is xresxyres.
  • The trigger. cocoa's zoom-to-fit defaults to off.

    • With it off, the window is the guest's pixels divided by the backing scale, and it cannot be resized. A 1280x720 guest is 640x360 points: the tiny picture.
    • With it on, cocoa scales the guest's image to the window, which is the soft picture, and asks the guest to follow.
    • GTK defaults zoom-to-fit to on for virtio-gpu (qapi/ui.json).
    • While the cocoa window is dragged it keeps the guest's current aspect ratio. Full screen takes any.
  • GTK reports logical pixels and no mm. On a HiDPI Linux host the guest is offered a smaller mode at 100 dpi, and GTK scales it up. That is QEMU's to fix, not ToyOS's.

  • OVMF publishes no EDID protocol on its -vga std GOP (QemuVideoDxe/Initialize.c:380 is a TODO). The GOP profiles and metal-sim boot with no panel size.

Measured with QEMU 11.1.1's qemu-edid, which calls the same generator the device answers GET_EDID with:

input base block, cm (bytes 0x15, 0x16) first 18-byte descriptor
default (1280x800, 100 dpi) 32 x 20 a timing, 325 x 203 mm
2560x1600 at 254 dpi 25 x 16 a timing, 256 x 160 mm
1920x1080 at 157 dpi 31 x 17 a timing, 310 x 174 mm
3456x2234 at 254 dpi 34 x 22 not a timing: pixel clock 0, tag 0xF7. The mode is in a DisplayID extension in the third block, which carries no size.

A full-screen window on a fast panel crosses QEMU's large-screen line: 4096 pixels, or a 655.36 MHz pixel clock. So the parser must tell a descriptor from a timing. When there is no timing, the size comes from the base block.

Not measured:

  • The live cocoa event sequence on the development host. Agents launch no QEMU display, so it is the owner's reading in S1's checks.
  • The host's built-in panel reports as Retina (system_profiler).

Order

  1. M1, a metal reading, before S2's GOP half.
  2. S1: virtio-gpu leaves the kernel and the compositor drives it. Live mode changes then work.
  3. S2: the scale.

S2 could come first only if the kernel driver copies its boot-time GET_EDID into the claim. That is about 20 lines, which S1 then deletes. I recommend S1 first.

Who owns what

part owner
the virtio-gpu function: queues, config interrupt, display info, EDID, resources, scanout, cursor the compositor, through a pci:1af4:1050 claim; the driver is the crate system/compositor/virtio-gpu on toyos-virtio (S1)
the GOP scanout the kernel, which maps the firmware's memory and mints the framebuffer claim. The claim carries the EDID block the loader read, unparsed (S2)
decoding an EDID lib/edid, a crate of its own because it decodes a device's bytes (S2)
the output's mode and scale, and the layout at that scale the compositor's pure crate, toyos-desktop (S1, S2)
a window's physical size and scale on the wire toyos-window's WindowInfo and ResizeInfo (S2)
winit apps the winit fork's ToyOS backend: scale_factor and ScaleFactorChanged (S2)
glyphs at a size toyos-font, from the TTF (S2)
icons and the cursor at a size sprite (S2)
the user's override screen scale; the compositor answers it and keeps it (S2)

M1. Each laptop's panel size, and whether its firmware publishes it

  • One T14 boot and one AMD bench boot of a scratch build that never lands. The loader opens EFI_EDID_ACTIVE_PROTOCOL and EFI_EDID_DISCOVERED_PROTOCOL on the GOP's handle and prints:
    • whether each is there;
    • SizeOfEdid;
    • the base block's sizes.
  • The panels' own EDIDs, committed as lib/edid fixtures. The T14's is read under its Ubuntu (/sys/class/drm/card*-eDP-*/edid); the AMD bench's is read the same way where it boots Linux. Before committing:
    • the ID serial (bytes 0x0C-0x0F) and any 0xFF serial descriptor are zeroed;
    • the checksum is recomputed;
    • the provenance says both were done.
  • Why: S2's GOP half rests on a protocol that QEMU's firmware does not publish, and no boot of either laptop has asked for it.
    • If neither firmware publishes it, S2's GOP half is the override alone, until a display driver reads the panel over its own link.
    • The next source to measure is then ACPI _DDC on the panel's device, which acpiserver can evaluate.

S1. virtio-gpu leaves the kernel; the compositor drives it and follows the host's window

Issue: stage 1 of issues/every-driver-is-still-in-the-kernel.md, its virtio-gpu half. S1 also meets the exits of the-gpu-driver-asserts-on-what-the-device-answers.md and the-second-scanout-buffer-is-read-by-nobody.md.

Design.

  • The holder is the compositor.

    • The compositor's row in system.toml gains pci:1af4:1050 beside framebuffer.
    • It takes virtio first, as soundserver does, since no machine here has both.
    • A separate display server was considered. Every present would then be an IPC hop to a process with one client. A Wayland compositor drives KMS itself, and soundserver and netstack drive their functions in-process.
  • The driver is system/compositor/virtio-gpu, beside system/soundserver/virtio-sound.

    • The commands are types.
    • Every answer other than the expected one is refused by name.
    • Every size the device reports is bounded before it is used as a size.
    • It is written against toyos-virtio's Registers and DmaBuffers, and host-tested on its stub.
  • One MSI-X entry serves both the control queue and configuration changes (Setup::config_vector, as netstack does). The claim's record joins the compositor's poller beside the keyboard and mouse.

  • The output is read the same way at start and on every event (virtio 1.2 §5.7.4.2, §5.7.5):

    1. Write events_clear with what events_read holds.
    2. Then send GET_DISPLAY_INFO and GET_EDID(0).

    Clearing first means an event that lands during the reads raises another notification and is not lost.

    • The mode is the display info's preferred size (§5.7.6.8: in a VM it follows the host window).
    • The EDID gives only the size in mm (S2).
    • The 1280x720 floor goes.
  • A mode change is a page flip (§5.7.6.3). The old mode stays on screen until SET_SCANOUT:

    1. Lend a new backing, then RESOURCE_CREATE_2D (the next id) and RESOURCE_ATTACH_BACKING.
    2. Make a new back buffer, desk and window layout (S2's relayout), and compose a whole frame into the new backing.
    3. TRANSFER_TO_HOST_2D (whole), SET_SCANOUT, RESOURCE_FLUSH (whole).
    4. RESOURCE_UNREF the old resource, then unlend its backing and drop it. The IOMMU mapping goes even if the device refuses the unref, so the device never reaches those pages again.

    A refusal before step 3 unrefs the new resource, drops its backing, keeps the old mode, and is logged by name. There is one backing per mode, so the second scanout goes.

  • A present is TRANSFER_TO_HOST_2D and RESOURCE_FLUSH per damaged rectangle.

    • A frame's commands are queued at once and awaited on the claim's record, bounded by a timeout that fails loudly.
    • The next blit waits until the last transfer completes, so the host never copies a half-written frame.
  • The cursor is the cursor queue's UPDATE_CURSOR and MOVE_CURSOR, sent by the compositor.

  • MSG_SET_RESOLUTION takes the same flip at the asked size on virtio.

    • On GOP it is refused by name, where today it is dropped without a word.
    • The next display event's size wins over it.
  • What the kernel loses:

    • drivers/virtio_gpu.rs and the Gpu trait;
    • the four SYS_GPU_* numbers and toyos::gpu;
    • the framebuffer claim's cursor region and its second scanout;
    • the panic console's detach on a mode change.

    GOP stays as the claim's one scanout. The compositor presents it with its own write-combining blit and sfence; SYS_GPU_PRESENT was already a no-op for it.

  • pcidev's bounds.

    • Grants stay at 8 MiB each and 32 MiB per claim.
    • A lend is memory the process already holds and is charged for, so it is bounded only by the claim's lend window.
    • That window grows to two scanouts at the compositor's largest mode, plus the rings. For 7680x4320 that is 258 MiB of address space, which costs page tables only for what is mapped.
    • A mode past the largest is refused by name, where the compositor bounds what the device reports.
  • The issue's control obligation lands in this diff, as the issue states. The IOMMU decline is made in toyos-virtio's Offer::accept for a function a process drives, and it reds when that function keeps FEATURES_OK. Profile::HeadlessVirtioGpu goes.

  • The development host. On macOS, build/qemu.rs passes -display cocoa,zoom-to-fit=on, which is what QEMU already gives GTK for this device by default. xres=1280,yres=720 stays, as the size before the window reports.

Roast.

  • This is the tracked stage, and it is bigger than the feature asked for. The alternative keeps the driver in the kernel:

    • the kernel driver takes the config interrupt;
    • the claim becomes pollable for display changes;
    • the claim's description gains the display info and the EDID.

    That is new kernel code and new SYS_GPU_* ABI for work that netstack and soundserver show userland owns. It also goes against the issue's ruling that no syscall carries a GPU operation. Rejected.

  • A GPU driver bug now ends the compositor, not the kernel. The desktop cannot outlive its display, and the kernel no longer dies on a device's answer.

  • The compositor blocks on its own device during a flip and during each frame's transfers, bounded and failing loudly. The server doctrine forbids blocking on a client, not on the server's own device.

  • The lend window grows for every claim, not only the display's. It is address space. The grant bound, which is what protects memory, is unchanged.

  • The Virtio profile then uses four of four PCI function slots. The next PCI claim on it needs MAX_FUNCTIONS raised.

  • The panic console still shows nothing on the Virtio profile. OVMF's virtio-gpu GOP is Blt-only and publishes no framebuffer, as today.

  • screen WxH and the host window can disagree, and the later one wins. With zoom-to-fit, a mode the user set lasts until the window is next resized.

Checks.

  • Host: the driver on toyos-virtio's stub:
    • the start and every event clear before they read;
    • an event raised between the clear and the reads is seen on the next pass;
    • each refused answer is refused by name and leaves the old mode;
    • the flip's command order, with the old backing unlent only after SET_SCANOUT and RESOURCE_UNREF;
    • a display size of 0, or past the bound, is refused by name;
    • a disabled scanout keeps the mode.
  • Guest: the whole suite. Every desktop and screen test then runs on the userland driver, and that is its smoke test.
  • Negative control:
    • the decline control reds with the decline removed;
    • named mutations turn the stub tests red: clearing after the reads, unlending before SET_SCANOUT, and accepting an error answer.
  • Oracle: virtio 1.2 §5.7 for the order and the fields, and QEMU 11.1.1 as the device under the guest suite.
  • The owner's reading: run cargo run on the Mac, drag the window, and go full screen. The compositor logs one line per event naming the mode, the mm and the scale, and the picture is sharp.

S2. The scale

Issue: stage 5 of issues/toyos-has-a-desktop.md.

Design.

  • The model is fractional-scale-v1's, physical-first.

    • The compositor composes in the panel's pixels and never scales a client's buffer.
    • A client is told its buffer's physical size and the output's scale as n/120, fractional-scale-v1's representation.
    • The client renders at that scale into a buffer of exactly that size. A fractional-scale-v1 client does the same behind wp_viewport, and so does a Windows app that is per-monitor DPI aware (v2).
    • Integer scales take the same path, with n a multiple of 120.
  • macOS's model was considered and rejected. There, apps render at an integer backing scale (1 or 2), and the window server resamples the whole screen to a non-integer "looks like" mode.

    • On CPU rendering, 1.25 on a 1080p panel would mean rendering 3072x1728 and resampling 2.56 times the panel's pixels every frame.
    • Resampled text is softer than text drawn at size.
  • No compositor upscaling of a client that ignores the scale, the way Wayland upscales a buffer_scale 1 surface. Every ToyOS client renders at scale. A third-party app that ignores scale_factor draws small, as it would on Windows.

  • The compositor decides the scale, purely, in toyos-desktop:

    • Auto is Mutter's rule (calculate_scale). Mutter's comment says its 135 dpi target matches the defaults of macOS, Android, iOS and Windows.
      • Take the panel's density over 135 dpi under a 20-inch diagonal, and over 110 dpi at or above it.
      • Round to the nearest quarter step from 1 to 4 that leaves at least 600x600 logical pixels.
      • If the image size is an aspect ratio, or there is none, the scale is 1, logged by name. An aspect ratio is EDID's zero size field, or one of Mutter's 16x9-shaped sizes. wl_output says the same: "if it is not sent, the client should assume a scale of 1".
    • Mutter's further filter is dropped. It keeps only scales that divide the screen exactly, because a Wayland output has an integer logical size. A ToyOS output has none: everything on the wire is physical.
    • Override: screen scale 1.25 or screen scale auto sends MSG_SET_SCALE.
      • The compositor applies it and keeps it in its own HOME (/state/compositor): one writer and one reader.
      • A value that is not a quarter step from 1 to 4 is refused by name.
    • What the rule gives (scale-rule.log):
      • QEMU's default EDID, and the harness's -display none: 1;
      • a 14-inch 1080p panel of 309x174 mm: 1.25. This is an assumed size; the T14's own figure waits for M1;
      • a 254-dpi cocoa window: 1.5 at 1280x720, 2 at 2560x1600;
      • a full-screen 3456x2234: 2;
      • a 27-inch 4K panel: 1.5.
  • EDID is decoded by lib/edid, which is no_std and has no unsafe:

    1. It checks the base block's header and checksum.
    2. It reads the image size. If the first 18-byte descriptor has a nonzero pixel clock, it is a detailed timing, and its millimetres are the size. Otherwise the size is bytes 0x15 and 0x16, in centimetres.

    Nothing else is read: the mode comes from the device or the firmware.

  • GOP's EDID travels unparsed:

    1. The loader copies EFI_EDID_ACTIVE_PROTOCOL's base block before ExitBootServices.
    2. The kernel passes it through the boot handoff into FramebufferInfo, as 128 bytes and a length.
  • Rounding is fractional-scale-v1's: half away from zero, in integers. px(l) = sign(l)·((|l|·n + 60) / 120). A rectangle scales by its corners, not by its origin and size, so two rectangles that touch still touch at any scale.

  • The wire (toyos-window):

    • WindowInfo and ResizeInfo gain scale. A window whose size or scale changed gets a new buffer and MSG_WINDOW_RESIZED, as a size change does today.
    • CreateWindowRequest's size is logical, as winit's Wayland backend takes a new window's size. The compositor converts it.
    • Input coordinates and damage stay in the window's physical pixels, as winit's CursorMoved is.
    • ResolutionInfo gains scale, so screen prints 1920x1080 @ 1.25.
    • Window gains scale_factor() -> f64. Event::Resized now means the size or the scale changed.
  • The winit fork gets one commit appended to toyos-0.30.13:

    • scale_factor() answers the window's scale.
    • On a resize whose scale changed, it sends ScaleFactorChanged and then Resized.
    • A requested LogicalSize goes on the wire as it is. A PhysicalSize goes as logical at 1.0, as the Wayland backend does.
    • The InnerSizeWriter's answer is not honoured, as request_inner_size is not today: the compositor decides window sizes.
  • The compositor draws at scale:

    • Chrome: every Chrome metric is px(DEFAULT). The desk, back buffer, wallpaper and window budget follow, as they do on a mode change today.
    • Text: toyos-font rasterizes JetBrains Mono from its TTF into a cell of px(8) x px(16).
      • It uses the algorithm the build runs now, build/assets.rs's rasterize_cells, moved into the crate, so the build and the guest share one producer.
      • The screendump decoder keeps reading the table the guest drew at scale 1.
      • fontdue becomes a runtime dependency of toyos-font. It is already in Cargo.lock for the build, calc and snake.
    • Icons and the cursor: sprite rasterizes each SVG at px(size), using build/icons.rs's rasterizer moved into it, and the build ships the SVGs. That file's header promises "no program carries a rasterizer while no icon is drawn at a size the build did not know"; a scale is what ends that.
    • The hardware cursor is used while px(20) fits virtio's 64x64, which is up to 3.2x. Past that the cursor is drawn in software. The hotspot scales.
  • Relayout is pure, in toyos-desktop:

    • On a scale change, a normal window's rectangle scales by n'/n and is then reflowed onto the work area.
    • A maximized or snapped window is retargeted, as on a mode change today.
    • A mode change alone reflows windows as it does today.
  • Nothing flashes.

    • A window keeps the buffer it last presented until its first present into the new one. Until then the compositor draws the old buffer, clipped, into the new rectangle.
    • A window has at most two buffers. A second resize before a present drops the buffer that was never presented. Its client still holds that buffer until it reads the newer message.
  • ToyOS's own clients render at scale.

    • terminal and filepicker, with its consent prompt, convert in S2: their text goes through Font at the scale, and their layout constants through px.
    • editor, files and paint are the owner's choice. Either they convert in S2, or they render at 1 (small text in a larger window) until the desktop track's stage 3 replaces them. That is visible, so it needs his sign-off.
    • console stays at 1: it draws on a raw scanout with no compositor.
  • inspect display gains scale.

Roast.

  • Mutter's density targets are another OS's policy, made ToyOS's default. The tests assert ToyOS's rule as stated at its site, not Mutter's output. They are cited because they are the published state of the art, and because Mutter says they match four other platforms.
  • Quarter steps only. KDE offers 5% steps. The wire carries n/120, so finer steps would be a policy change, not a wire change.
  • Physical-first puts a non-integer logical size in every client's hands. winit already deals in LogicalSize<f64>. A fractional-scale-v1 client avoids that, at the cost of a rounding contract across the wire, which ToyOS then does not need.
  • A normal window can drift by one pixel when the scale goes down and back up. round(round(x·a)/a) is exact for a ≥ 1 but not for a < 1.
    • Keeping each window's size in logical units would remove the drift, at the cost of converting the desktop crate's geometry.
    • Not taken.
  • Two rasterizers move into the guest. The alternative is the build drawing every font and icon at all 13 quarter steps.
    • For the font, that is Σs² = 92.6 times the 1x table. Snake's build of that table is 54,920 bytes, so about 5 MB.
    • For icons, it is a set of sizes that has to track the scale policy.
    • At run time, rasterizing costs milliseconds per scale change.
  • A window holds two buffers during a resize. A client that never presents again holds both until it ends, and the window budget does not count the second.
  • calc and snake draw build-rasterized fonts at a requested PhysicalSize. At 1.25 they open larger with text the same size, as they do on a Retina Mac today. Fixing them is their job, not the platform's.
  • The T14's 1.25 rests on M1. If its firmware gives no EDID, it boots at 1 until the user sets the override or a display driver reads the panel.
  • QEMU's GOP profiles have no EDID, because OVMF publishes none. So metal-sim boots at 1 while the T14 boots at its own scale, and a screen test on metal-sim does not see the T14's scale. The suite reads text at 1, which is what it gets.
  • The scale's authority is the compositor connector, the same as MSG_SET_RESOLUTION's today. Any program that holds the connector can change both.

Checks.

  • Host:

    • lib/edid:

      • the refusals: bad header, bad checksum, short block;
      • the detailed timing's mm;
      • the cm when the first descriptor is not a timing;
      • an aspect ratio refused.

      Its fixtures are QEMU 11.1.1's four EDIDs, committed with the commands that made them (above), and M1's panels.

    • Scale:

      • the rule's results above as tests, plus M1's panels;
      • rounding at the halves and at negative coordinates;
      • corner scaling of touching rectangles;
      • override parsing and its refusals.
    • relayout:

      • a normal window keeps its logical size to within a pixel and stays on the work area;
      • a maximized window fills the new work area;
      • the cursor is clamped.
    • the window's buffer swap: at most two buffers, and the old one dropped at the first present after a resize.

  • Guest: the whole suite, at scale 1, which QEMU's EDID gives it. No new guest test.

  • Metal:

    • M1's lines;
    • inspect display on each laptop, reading the scale its EDID gives.

    No row: nothing is timed.

  • Negative control:

    • each rule's test, run against the change reverted onto ab655a115;
    • mutations named per test in the build: rounding half to even, skipping the cm path, and dropping the old buffer at the resize.
  • Oracle:

    • QEMU's generator and the real panels' EDIDs, for the parser;
    • VESA E-EDID 1.4 and fractional-scale-v1, for the byte offsets and the rounding.

Metal, now and later

  • T14: Iris Xe, 8086:9A49 (issues/toyos-has-a-desktop.md), on the firmware's GOP at the panel's 1920x1080.
  • AMD bench: an AMD APU whose display audio is 1002:1637. Its GPU's id, its panel and its GOP mode are not in the tree.
  • Now, on both:
    • A scale, from the firmware's EDID if M1 finds one and from the override otherwise. It can change live.
    • No mode change. The mode is the firmware's (owner, 2026-09-01, issues/gop-is-not-the-path-every-boot-takes.md), and the kernel makes no UEFI call after the loader.
    • No hotplug and no vblank, so the blit can still tear against the beam (issues/tearing-is-what-gop-cannot-give-back.md).
  • Later: one display driver per display engine. The T14's Xe-LP is the desktop track's stage 12; an AMD display engine driver is tracked nowhere.
    • Each joins the compositor's Display as another variant, bringing EDID over the panel's own link, a mode list, flips on vblank, and hotplug interrupts.
    • The flow above takes them unchanged. Multiple outputs are the desktop track's stage 8.

What I am unsure of

  • Whether S1 is wanted now, or S2 first with the 20-line kernel copy.
  • 7680x4320 as the largest mode. It is chosen to cover every panel a Mac host can put QEMU on. QEMU's 256 MiB max_hostmem holds a pair at that size with under 3 MB to spare.
  • The override's home: the compositor's HOME, or /config beside the keyboard layout. The layout is in /config because every translator reads it; the scale has one reader.
  • The section numbers in E-EDID 1.4 (§3.6.2, §3.10.2), UEFI 2.10 (§12.9's EDID protocols) and ACPI 6.5 (§B.6.3, _DDC) are from memory. Those pages could not be fetched, and each is checked against the standard and cited at its site before the build.
  • QEMU's 1 s debounce is its own. Whether a drag feels live enough is the owner's reading.
  • An AMD display engine driver is not filed anywhere; whether to file it is the orchestrator's call.

Gates

Design round: nothing built, and no gate run. git diff --shortstat origin/main...HEAD is empty.

🤖 Generated with Claude Code

https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy

The design round's empty commit. The design and its roast are the pull
request's body; the build drops this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

The orchestrator, on the design's two decisions:

  1. Order: S1 first. virtio-gpu moves into the compositor as issues/every-driver-is-still-in-the-kernel.md stage 1 plans, and S2's scale follows. No kernel lines are added only to be deleted. The 32 MiB lending limit is raised in S1, on its merits.
  2. Apps: files (the only desktop app staying in the tree) renders at scale with S2. editor and paint are leaving the tree as packages (The package repository's keys: toyos-root signs root 1 offline, and the publisher signs under it alone (stage 1 of apps leaving the tree) #892), so they stay at scale 1 until they read the scale from the surface API in their own repositories.

@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Design review, round 1: head c59ecd3b3, on ab655a115. git diff --shortstat origin/main...c59ecd3b3 is empty, so production and tests are 0 and 0. main has since moved to 97f07c381 (#888), which changes prompts only.

Read against:

  • virtio 1.2 cs01 §5.7;
  • QEMU v11.1.1 at c3d48b7: ui/cocoa.m, ui/console.c, hw/display/virtio-gpu.c, virtio-gpu-base.c, edid-generate.c, qemu-edid.c and qapi/ui.json;
  • evidence 1ba8ddc4, and the tree.

The orchestrator's rulings are taken as given: S1 first, the lending limit raised in S1, and files at scale in S2.

Weighed

  • S1 against issues/every-driver-is-still-in-the-kernel.md. It is the issue's stage 1, done as the issue says:

    • the compositor claims the function as pci and drives it in-process, as netstack and soundserver do;
    • SYS_GPU_* goes and GOP stays.

    It drops three of the issue's obligations: the len measurement (B3), the control's mechanism (B6), and the suite's only virtio-gpu boot (B2).

  • The 32 MiB limit. Bounding lends by the window alone is right, because a lend allocates nothing. The reason the design gives is false (N1).

  • The resize flow, end to end.

    • Tearing, and device reads after the unlend: B1.
    • A lost display event: B4.
    • A window buffer the budget does not count: B7.
    • Refusals the design leaves unspecified: N5.
  • The large-screen EDID. The parse reads it right. There is no detailed timing, so it takes bytes 0x15/0x16 in cm, and QEMU's own fixture holds that case. The mode never comes from EDID. Two things stay open: a timing with a zero size (N3), and the refresh rate the fixtures were made at (N2).

  • cocoa. The source reading holds where the design depends on it:

    • the size is in backing pixels;
    • the mm come from CGDisplayScreenSize;
    • reports are debounced by 1 s;
    • the window is resizable only with zoom-to-fit.

    It misses the safe area and the refresh rate (N2), and how far zoom-to-fit reaches (B8). The owner's reading stays the only live check, which is right while agents open no display.

Where this review rests on Linux. In one place: I read GNOME Mutter's src/backends/meta-monitor.c at 65fc6baa to check the body's account of calculate_scale. B10 asks that the rule stop resting on Mutter, so no other finding depends on that reading.

  • B1, B3 and B4 rest on virtio 1.2 and QEMU's source.
  • B8 rests on QEMU's cocoa source and QEMU's own option documentation.
  • N2 and N3 rest on QEMU's generator and on VESA E-EDID 1.4 as the design quotes it.
  • N8's DisplayID container ID is VESA DisplayID 2.0 from memory, not read here.

The design rests on Linux in three places: B10 (Mutter), N8 (M1 through Ubuntu's sysfs) and N9 (KMS).

BLOCKER

  1. PR body, S1 "A present" and the flip's step 4; kernel/src/drivers/virtio_gpu.rs:61-62,507-509: completion is assumed without VIRTIO_GPU_FLAG_FENCE.

    • The design treats a command's used-ring return as done in three places:
      • "the next blit waits until the last transfer completes";
      • the old backing is unlent once RESOURCE_UNREF returns;
      • the cursor image is uploaded before UPDATE_CURSOR.
    • It sets the fence flag nowhere, like today's driver, whose CtrlHeader::new writes flags: 0.
    • virtio 1.2 §5.7.6.5: "The device MAY process controlq commands asyncronously and return them to the driver before the processing is complete … set the VIRTIO_GPU_FLAG_FENCE flag … The device MUST finish the processing before returning the command then." §5.7.6.6 and §5.7.6.10 require the cursor's fill to be fenced before UPDATE_CURSOR.
    • Without the fence, a conforming device can still be reading the backing during the next blit, which tears the frame. Or it can read it after the unlend: that access faults at the unit, and the claim then answers Io, "the one answer its holder cannot take for a quiet device" (kernel/src/pcidev/mod.rs:1727-1729).
    • QEMU's 2D path finishes synchronously (the spec's own note), so no guest run can show this.
    • Change:
      • These four commands carry the flag and a fence_id: the frame's last TRANSFER_TO_HOST_2D, the flip's transfer before SET_SCANOUT, the UNREF before its unlend, and the cursor's transfer.
      • An answer without the flag, or with another fence_id, is refused by name.
      • A stub test covers each, and the mutation "drop the flag from the UNREF" turns one red.
  2. tests/common/qemu.rs:1057: after S1, no guest test boots a virtio-gpu at all.

    • The only shape with virtio_gpu: true is Profile::HeadlessVirtioGpu. Every display profile is vga: "std", and Desktop derives from Metal (:1029, :1047).
    • HeadlessVirtioGpu's only user is declining_is_not_free (tests/common/iommu.rs:249-256), and S1 deletes the profile.
    • So two of the design's claims are false of the tree:
      • "Every desktop and screen test then runs on the userland driver, and that is its smoke test";
      • "QEMU 11.1.1 as the device under the guest suite".
    • The userland driver would be checked only against the stub, which encodes the design's own reading of §5.7.
    • No type, host test or metal row reaches a real virtio-gpu, because no metal machine has one. Integration between modules is tested only for the kernel, and an integration test outside it is cut #890 keeps room for "a very cheap smoke test".
    • Change: one guest smoke boots the compositor on a virtio-gpu machine and reads that it set its first scanout and presented a frame: its mode line, and a QMP screendump that is not blank. That reason goes in the body.
  3. QEMU hw/display/virtio-gpu.c:1221-1250: QEMU writes no cursor-queue answer.

    • QEMU answers every cursor-queue command with used len 0 and writes no response, although §5.7.6.10 says UPDATE_CURSOR and MOVE_CURSOR answer RESP_OK_NODATA.
    • The design's "Every answer other than the expected one is refused by name" would then refuse every cursor command on QEMU.
    • The issue's stage 1 also requires, before a holder ends its device on UsedRefusal::Written for a chain the device only reads, a measurement of what QEMU reports as len on that queue. The design does not carry it.
    • Change:
      • cursor-queue chains are device-read-only (bound 0), and no answer is read;
      • S1's body carries the measured len;
      • a stub test refuses a cursor-queue element with len > 0 as Written.
  4. PR body, S1 "One MSI-X entry serves both the control queue and configuration changes": a display event can be lost.

    • One entry serves the control queue, the cursor queue and configuration changes, so a wake of the claim's record does not say which one fired.
    • The flip and every frame wait on that same record. A configuration notification that lands while a transfer is awaited is consumed by that wait.
    • The design reads events_read only "on every event", so that resize is lost until the host window changes again.
    • Change:
      • events_read is read after every wake of the record, inside the bounded waits too;
      • stub test: "a display event raised while a frame's transfer is awaited is read on the next pass";
      • the mutation "read events_read only on a wake with no used element" turns it red.
  5. images/system.toml:37; system/compositor/src/session.rs:880-888: every installed package can change the display.

    • MSG_SET_SCALE goes on the compositor connector, which [apps] hands every installed package. The compositor keeps the value in /state/compositor, so any package can change the machine's display scale, and the change persists.
    • MSG_SET_RESOLUTION is answered from any window connection today. After S1 it becomes a client-driven flip that lends up to two 128 MiB contiguous scanouts.
    • The roast names this ("Any program that holds the connector can change both") and leaves it. That is an isolation hole the design widens.
    • Change: the display's settings get a port of their own that only toybox receives (its screen applet), served beside compositor as prompt is (images/system.toml:76). The window protocol drops both messages.
  6. PR body, S1 "The issue's control obligation lands in this diff"; kernel/src/actuator.rs:127, kernel/src/drivers/virtio.rs:166: the decline control has an exit but no mechanism. The design restates the issue's exit but not how a process's Offer::accept is made to decline. Each way of doing it carries an obligation:

    • A runtime switch in toyos-virtio ships for tests alone.
    • A cargo feature needs its build/clippy.rs shape, and a holder built with it only into a test image.
    • The changed guest control needs the body's reason why no host test reaches QEMU's virtio_validate_features.
    • The design does not say which function on which machine declines once HeadlessVirtioGpu goes.
    • The kernel actuator virtio_no_access_platform then reaches nothing, since the console is exempt from it, so it is dead code that S1 deletes.

    Change: name all of these.

  7. system/compositor/desktop/src/budget.rs:11,23,44: the window budget no longer holds.

    • max_windows holds window buffers to an eighth of memory, at one screen-sized buffer per window.
    • S2 gives a window two buffers:
      • during a resize;
      • for every window at once on a scale change;
      • for as long as a client that never presents again lives.
    • The roast states that the budget does not count the second buffer, and nothing records this.
    • Change: window_bytes counts two buffers, with its host test. That removes the compromise in one line.
  8. QEMU ui/cocoa.m:2000-2002, 498-513, 1167, 103: zoom-to-fit degrades the GOP and metal-sim picture.

    • "On macOS, build/qemu.rs passes -display cocoa,zoom-to-fit=on" names no profile.
    • On Gop and Metal the guest cannot follow, and the resizable branch fits it into the initial 640x480-point view. metal-sim's 1920x1080 then shows in 640x360 points, which is 1280x720 backing pixels, downscaled with kCGInterpolationNone.
    • Today it shows 1:1 in 960x540 points. That is a loss of visual quality without the owner's sign-off.
    • Change: zoom-to-fit is on only where Shape::virtio_gpu holds, which is the default QEMU documents for GTK (qapi/ui.json:1327-1331).
  9. apps/files/src/icon.rs:12-21; build/icons.rs:1-5: S2 breaks files' icons or adds a second rasterizer.

    • The ruling puts files at scale in S2.
    • files draws /system/share/icons/<stem>.alpha through its own near-copy of sprite.
    • S2 has the build ship the SVGs and sprite rasterize them, so files either loses its icons or grows a second rasterizer.
    • Change: one rasterizer crate used by the compositor, files and the build. sprite moves out of system/compositor/, and files' icon.rs folds into it.
  10. PR body, S2 "Auto is Mutter's rule (calculate_scale)" and "one of Mutter's 16x9-shaped sizes"; scale-rule.log: the auto scale rests on Mutter.

    • The default scale and the aspect-ratio test rest on GNOME Mutter's code (meta-monitor.c:2421-2503, meta-monitor-manager.c:1662-1675), a Linux desktop's implementation, cited by file and line and to be stated at the rule's site.
    • Under the owner's direction, ToyOS follows no Linux source, and the tree carries no reference to one.
    • Change:
    • The body's account of Mutter is also inexact, which stops mattering once the rule leaves it: Mutter's candidates are every n/d with d ≤ 4, thirds included (meta-monitor.c:33, :2592-2628). For cocoa-small, its filtered set gives 4/3, not the log's 1.25.

NOTE

  1. PR body, S1 "pcidev's bounds": the lend window's premise is false.
    • "A lend is memory the process already holds and is charged for" is false: nothing charges memory to a process (issues/no-physical-memory-fairness.md; budget.rs:6-9).
    • The conclusion holds for another reason: dma_map allocates nothing (kernel/src/pcidev/mod.rs:1648-1672), so the window is only address space in the slot's own domain.
    • 7680x4320 is chosen for "every panel a Mac host can put QEMU on". State it on the target display engines' published maximum instead, and make it one declaration that the kernel's window and the compositor's refusal both read.
    • Each lend is one physically contiguous run (kernel/src/object/shm.rs:92), and a flip needs a fresh one while the old is still held. Either name that refusal after fragmentation as expected, or back the resource with several lends: §5.7.6.1 allows scatter lists.
  2. PR body, "What QEMU does": the cocoa reading misses two things, and the fixtures are at the wrong refresh.
    • Full screen reports the safe area, not the screen (cocoa.m:489-496, :542).
    • The panel's refresh reaches the EDID generator (cocoa.m:544-550; virtio-gpu-base.c:67,111).
    • At 120 Hz, a 2560x1600 window already crosses the large-screen line: 120000·3456·1656/10⁷ = 68,677 ≥ 65,536 (edid-generate.c:418). So it gets no detailed timing.
    • The fixtures are at qemu-edid's fixed 75 Hz: qemu-edid.c:50 has no refresh option, and the generator defaults to 75 Hz (edid-generate.c:390).
    • So the 2560x1600 row is not what the device answers on such a host, and the 3456x2234 row is not what cocoa sends on a notched panel.
    • The design stands, because the parse has both paths. The owner's S1 reading should also dump the GET_EDID bytes, so that a fixture comes from the device itself.
  3. PR body, S2 "EDID": a zero-size timing returns scale 1. A detailed timing whose image size is 0 is "not given". The rule takes that 0 and returns scale 1, even where bytes 0x15/0x16 carry a size. Fall back to the base block, with a test.
  4. PR body, S1 "Every size the device reports is bounded" and S2 "Override": no lower bounds.
    • Sizes are bounded only at 0 and at the top.
    • On a 1x host, cocoa's initial 640x360-point view makes the boot mode 640x360.
    • screen scale 4 on 1280x720 leaves 320x180 logical pixels.
    • Name the least mode the desktop lays out, and hold the override to the floor the auto rule keeps. Say what a kept override does on a mode that cannot hold it, and host-test both edges.
  5. PR body, S1 flip: refusals after step 2 are unspecified. Only "a refusal before step 3" is specified. Specify and host-test each of these:
    • SET_SCANOUT refused after the whole-frame transfer;
    • RESOURCE_FLUSH refused after SET_SCANOUT;
    • a refused UNREF, which leaves the host resource charged to max_hostmem.
  6. kernel/src/pcidev/mod.rs:146: four of four PCI slots. The roast's "four of four PCI function slots" is a compromise the branch found. It is neither raised nor recorded in issues/ with an owner and an exit.
  7. system/console/src/main.rs:435-436; tests/guest/rust-tests/src/bin/abuse_gpu_resolution.rs:1: what removing SYS_GPU_* also takes. S1 must name both:
    • console's present goes, and images/console.toml then draws only on a GOP machine;
    • the guest test is cut, on the ground that the syscall it guards is gone.
  8. PR body, M1: read the EDID by ToyOS's own route.
    • The design reads the T14's EDID under Ubuntu's /sys/class/drm, which puts a Linux interface into a fixture's provenance.
    • Read it by ToyOS's own route instead: the loader's EFI_EDID_ACTIVE_PROTOCOL read that M1 already boots (UEFI), or ACPI _DDC.
    • Commit only the 128-byte base block the parser reads. An extension can carry a per-unit identifier (DisplayID's container ID), which the zeroing of 0x0C-0x0F and the 0xFF descriptor does not reach.
  9. PR body, S1 Design: drop the KMS reasoning. "A Wayland compositor drives KMS itself" reasons from Linux's KMS. The tree's own precedent (netstack, soundserver) and the issue carry the decision, so drop the sentence.

DESIGN NEEDS CHANGES: fence every completion the design relies on (§5.7.6.5); keep one virtio-gpu guest smoke; make the cursor queue read-only, with QEMU's len measured; read events_read on every wake; put the display settings on a port of their own; name the decline control's mechanism and delete the kernel actuator; count both window buffers in the budget; turn zoom-to-fit on for the virtio-gpu profile only; use one icon rasterizer for the compositor, files and the build; re-source the auto scale and the aspect-ratio test from vendor documentation or published specifications instead of Mutter

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant