display: hot-attach a virtio-gpu native backend over the GOP floor (v2 V4)

The compositor now boots on the GOP framebuffer and upgrades to the virtio-gpu driver
the moment it announces itself — the pluggable-scanout payoff.

The shared surface. The scanout resource is an shm region the driver creates
(shm_physical, a new syscall, hands it the guest-physical for attach_backing) and passes
to the compositor as a capability. The compositor maps it and composites straight into
it: on x86 DMA is cache-coherent, so the cacheable shared pages the CPU paints are exactly
what the device transfers-and-flushes — no copy, no explicit flush.

The handshake. After bring-up the driver looks up .display and sends attach_scanout with
the geometry + the surface capability. The compositor maps the surface, looks up the
driver's .scanout endpoint itself (the driver registered it — no need to pass it), switches
to backend.VirtioGpu, and re-composites the current frame. present() over the native
backend is a present request on .scanout -> transfer-to-host + resource flush. The first
native present is deferred to a one-shot timer: presenting inline from the announce handler
would deadlock, since the driver is still blocked on our reply and not yet serving .scanout.
After it lands, the compositor reads a pixel back from the shared surface to confirm the
frame reached the device's backing.

- shm_physical (syscall 36) + runtime.shm.physical.
- scanout-protocol (the compositor->driver present channel), separate from the
  client-facing display protocol; the display protocol gains attach_scanout.
- backend.VirtioGpu joins backend.Gop in the tagged union; select() still boots GOP.
- the virtio-gpu driver's scanout backing is now shm (was DMA); it announces + serves
  .scanout present requests (transfer-to-host + flush of the shared surface).

Also fixes a latent framebuffer-geometry corruption the display service hit only when it
enumerated the device tree alongside a busy device-manager: Gop.init now captures the
geometry into a small value the instant device_enumerate returns (rather than re-reading
the 328-byte descriptor across the later claim/mmio_map syscalls) and retries on a zero
geometry. The underlying device-table clobber is a separate kernel bug, tracked apart.

Gate: python3 test/qemu_test.py display-native (QEMU -device virtio-gpu-pci) — "display:
scanout upgraded to virtio-gpu" + "display: native present verified" + "display-demo: ok",
passing 3/3. host tests, display-service, display-demo, shm, and virtio-gpu still pass.
This commit is contained in:
Daniel Samson 2026-07-14 12:21:37 +01:00
parent 6e0e0a62c6
commit 58927ed7e5
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
13 changed files with 379 additions and 58 deletions

View File

@ -355,6 +355,13 @@ pub fn build(b: *std.Build) void {
});
runtime_module.addImport("display-protocol", display_protocol_module);
// The scanout protocol: the compositor's outbound present channel to a native scanout
// driver (virtio-gpu), separate from the client-facing display protocol (docs/display-v2.md).
const scanout_protocol_module = b.addModule("scanout-protocol", .{
.root_source_file = b.path("system/services/display/scanout-protocol.zig"),
});
runtime_module.addImport("scanout-protocol", scanout_protocol_module);
// The power protocol: system power's domain-named surface (docs/power.md).
const power_protocol_module = b.addModule("power-protocol", .{
.root_source_file = b.path("system/services/power/protocol.zig"),

View File

@ -100,21 +100,28 @@ pattern — logging `virtio-gpu: scanout 640x480 online` and `virtio-gpu: flush
check ok`. That proves virtqueue + resource + attach + set_scanout + transfer + flush end to
end without a screenshot (the used-ring ack is the device confirming it consumed the frame).
## V4 — The native backend + hot-attach
## V4 — The native backend + hot-attach
- [ ] `VirtioGpuBackend` in the compositor: `surface()` = the shared scanout resource,
`present(damage)` = `resource_flush` of the damaged rect.
- [ ] The driver **announces** to `.display` (looks it up, sends *attach-scanout* with its
`scanout` endpoint + the shared surface as capabilities). The compositor switches
backends and re-presents the current frame full-screen.
- [ ] Boot still starts on `GopBackend`; the upgrade happens on announce.
- [x] `backend.VirtioGpu` in the compositor: `surface()` = the shared `shm` scanout surface
(the compositor composes straight into the device's resource backing; x86 DMA is
coherent, so the cacheable shared pages need no flush), `present(damage)` = a `present`
request over the driver's `.scanout` endpoint (→ transfer-to-host + resource flush).
- [x] The driver **announces** to `.display` after bring-up (looks it up with a bounded retry,
sends `attach_scanout` with the geometry + the shared surface as an `ipc_call` send_cap).
The compositor maps it, looks up `.scanout` itself (no need to pass the endpoint — the
driver registered it), switches backend, and re-composites the current frame full-screen.
The present is deferred to a one-shot timer so it runs *after* the reply unblocks the
driver and it serves `.scanout` — presenting inline would deadlock.
- [x] Boot still starts on `backend.Gop`; the upgrade happens on announce. `shm_physical` (a
new syscall) gives the driver the guest-physical of the shared surface for `attach_backing`.
**Gate (automated):** boot with virtio-gpu + `display-demo`; the compositor logs
`display: scanout upgraded to virtio-gpu`, drives frames through the native backend, and
**reads a pixel back** from the shared scanout resource after a present to confirm the
composited frame landed (`display: native present verified`), while `display-demo: ok`
still fires. Without `-device virtio-gpu`, no `scanout` is announced and it stays on GOP —
the v1 `display-service`/`display-demo` gates still pass unchanged.
**Gate (met):** the `display-native` case (QEMU `-device virtio-gpu-pci`, `mem` bumped since it
boots the whole system) starts the compositor + `display-demo` + device-manager; the driver
announces, the compositor logs `display: scanout upgraded to virtio-gpu`, drives frames through
the native backend, and **reads a pixel back** from the shared surface after a present to
confirm the composited frame landed (`display: native present verified`), while `display-demo:
ok` still fires — checked order-independently. Without `-device virtio-gpu-pci` nothing is
announced and it stays on GOP: the v1 `display-service`/`display-demo` gates pass unchanged.
## V5 — Mode-setting, EDID, and vsync

View File

@ -56,6 +56,9 @@ pub const block = @import("block.zig");
pub const display = @import("display.zig");
/// The display wire protocol (shared with the display service and its clients).
pub const display_protocol = @import("display-protocol");
/// The scanout wire protocol: the compositor's present channel to a native scanout driver
/// (virtio-gpu). See system/services/display/scanout-protocol.zig and docs/display-v2.md.
pub const scanout_protocol = @import("scanout-protocol");
/// The danos-native file API (open/read/write/list over the user-space VFS) the
/// layer danos programs use directly, and where the operations that later become

View File

@ -45,3 +45,13 @@ pub fn map(handle: ipc.Handle) ?[*]u8 {
if (failed(r)) return null;
return @ptrFromInt(r);
}
/// The guest-physical base of the shared region named by `handle` (which this process must
/// hold a capability for). The region's frames are contiguous, so this single address plus
/// the region length is all a device needs e.g. a virtio-gpu driver programming an
/// `attach_backing`. Returns null on failure.
pub fn physical(handle: ipc.Handle) ?usize {
const r = sc.systemCall1(.shm_physical, handle);
if (failed(r)) return null;
return r;
}

View File

@ -62,6 +62,7 @@ pub const SystemCall = enum(u64) {
wall_clock = 33, // wall_clock() -> Unix epoch seconds (UTC): the RTC wall-clock time, for filesystem timestamps (mtime). Monotonic time is `clock`.
shm_create = 34, // shm_create(len) -> vaddr (rax), handle (rdx): a shareable, zeroed, cacheable RAM region mapped into this AS; the handle is a capability passed to another process as an ipc_call send_cap (docs/display-v2.md)
shm_map = 35, // shm_map(cap) -> vaddr: map the shared region named by a received capability into this AS (the same physical pages the creator sees)
shm_physical = 36, // shm_physical(cap) -> paddr: the guest-physical base of a shared region held by capability, so a driver can program it into a device (e.g. virtio-gpu attach_backing); the pages are contiguous (docs/display-v2.md)
_,
};

View File

@ -18,11 +18,18 @@ const runtime = @import("runtime");
const mmio = @import("mmio");
const device = runtime.device;
const dma = runtime.dma;
const shm = runtime.shm;
const system = runtime.system;
const ipc = runtime.ipc;
const dp = runtime.display_protocol;
const sp = runtime.scanout_protocol;
const vp = @import("virtio-pci.zig");
const vg = @import("virtio-gpu-protocol.zig");
/// The DisplayFormat (device-abi) our B8G8R8X8 scanout resource presents: bgrx = 1. Handed to
/// the compositor in the announce so it packs colours in the surface's byte order.
const display_format_bgrx: u32 = 1;
/// The PCI vendor/device ids of a modern virtio-gpu (Red Hat / virtio; GPU is a
/// virtio-1.0-only device, so the id is always the modern 0x1050 no legacy variant).
const virtio_vendor: u16 = 0x1AF4;
@ -59,10 +66,15 @@ var notify_addr: usize = 0;
// be asked to map the same resource twice.
var bar_virtual: [6]usize = .{ 0, 0, 0, 0, 0, 0 };
// DMA memory: the virtqueue rings, the command scratch, and the scanout backing.
// DMA memory: the virtqueue rings and the command scratch.
var ring: dma.Region = undefined;
var command: dma.Region = undefined;
var backing: dma.Region = undefined;
// The scanout backing is a **shared** (shm) region, not DMA: cacheable so the compositor
// composites into it cheaply (x86 DMA is coherent, so the device still sees the writes), and
// shareable so the same physical pages the device scans out of are the ones the compositor
// paints. The driver keeps the capability to hand to the compositor in the announce.
var surface: shm.Region = undefined;
// Split-virtqueue producer/consumer shadows.
var avail_shadow: u16 = 0;
@ -335,8 +347,14 @@ fn initialise(endpoint: ipc.Handle) bool {
}
}
backing = dma.alloc(scanout_bytes, dma.coherent) orelse {
log("virtio-gpu: scanout backing allocation failed\n", .{});
// Back the resource with a shared (shm) surface, so the compositor and the device work
// the same physical pages. The device needs the guest-physical base for attach_backing.
surface = shm.create(scanout_bytes) orelse {
log("virtio-gpu: scanout surface allocation failed\n", .{});
return false;
};
const surface_physical = shm.physical(surface.handle) orelse {
log("virtio-gpu: could not resolve the scanout surface physical address\n", .{});
return false;
};
{
@ -347,7 +365,7 @@ fn initialise(endpoint: ipc.Handle) bool {
.nr_entries = 1,
};
const entry: *vg.MemEntry = @ptrFromInt(command.virtual + request_offset + @sizeOf(vg.ResourceAttachBacking));
entry.* = .{ .addr = backing.physical, .length = @intCast(scanout_bytes) };
entry.* = .{ .addr = surface_physical, .length = @intCast(scanout_bytes) };
if (command_nodata(@sizeOf(vg.ResourceAttachBacking) + @sizeOf(vg.MemEntry)) != ok_nodata) {
log("virtio-gpu: resource_attach_backing failed\n", .{});
return false;
@ -368,12 +386,35 @@ fn initialise(endpoint: ipc.Handle) bool {
}
log("virtio-gpu: scanout {d}x{d} online\n", .{ scanout_width, scanout_height });
// Paint a known pattern, transfer it to the host resource, and flush it to the display.
const pixels: [*]u32 = @ptrFromInt(backing.virtual);
// Paint a known pattern, present it, and read it back the V3 self-test that proves the
// whole path (virtqueue, resource, shared backing, transfer, flush) before a client attaches.
const pixels: [*]u32 = @ptrCast(@alignCast(surface.ptr));
const pixel_count: u32 = scanout_width * scanout_height;
for (0..pixel_count) |i| pixels[i] = testPixel(@intCast(i));
mmio.wmb();
if (!presentFull()) {
log("virtio-gpu: initial present failed\n", .{});
return false;
}
// The scanout surface is CPU-visible RAM: read the pattern back to prove the mapping,
// which together with the flush ack above is the automated stand-in for "it's on screen".
mmio.rmb();
if (pixels[0] != testPixel(0) or pixels[pixel_count / 2] != testPixel(pixel_count / 2)) {
log("virtio-gpu: pixel read-back mismatch\n", .{});
return false;
}
log("virtio-gpu: flush acked, pixel check ok\n", .{});
// Offer the shared surface to the compositor so it upgrades off the GOP floor (V4).
announce();
return true;
}
/// Present the whole surface: copy the guest backing into the host resource, then flush it to
/// the panel. Reused by the V3 self-test and by every compositor present over `.scanout`. V4
/// presents the full surface; the damage-rect fast path is a later refinement.
fn presentFull() bool {
mmio.wmb(); // the surface writes must be visible before the device transfers them
{
const request = requestAt(vg.TransferToHost2d);
request.* = .{
@ -382,10 +423,7 @@ fn initialise(endpoint: ipc.Handle) bool {
.offset = 0,
.resource_id = resource_id,
};
if (command_nodata(@sizeOf(vg.TransferToHost2d)) != ok_nodata) {
log("virtio-gpu: transfer_to_host_2d failed\n", .{});
return false;
}
if (command_nodata(@sizeOf(vg.TransferToHost2d)) != ok_nodata) return false;
}
{
const request = requestAt(vg.ResourceFlush);
@ -394,30 +432,52 @@ fn initialise(endpoint: ipc.Handle) bool {
.rect = .{ .x = 0, .y = 0, .width = scanout_width, .height = scanout_height },
.resource_id = resource_id,
};
if (command_nodata(@sizeOf(vg.ResourceFlush)) != ok_nodata) {
log("virtio-gpu: resource_flush was not acked\n", .{});
return false;
if (command_nodata(@sizeOf(vg.ResourceFlush)) != ok_nodata) return false;
}
}
// The scanout backing is CPU-visible RAM: read the pattern back to prove the mapping,
// which together with the flush ack above is the automated stand-in for "it's on screen".
mmio.rmb();
if (pixels[0] != testPixel(0) or pixels[pixel_count / 2] != testPixel(pixel_count / 2)) {
log("virtio-gpu: pixel read-back mismatch\n", .{});
return false;
}
log("virtio-gpu: flush acked, pixel check ok\n", .{});
return true;
}
/// The `scanout` service exists from V3 so the compositor can find it in V4; it carries no
/// requests yet (the ping is answered by the harness), so anything else is a no-op.
/// Announce the scanout to the display service so it upgrades off the GOP framebuffer: hand it
/// the shared surface as a capability plus the geometry. Best-effort and non-fatal without a
/// display service (the standalone virtio-gpu bring-up test) the driver is still a valid
/// scanout service; it just serves no one. The display replies immediately (it defers its
/// first present to a timer), so this returns before we start serving `.scanout` no deadlock.
fn announce() void {
var tries: u32 = 0;
const display = while (tries < 50) : (tries += 1) {
if (ipc.lookup(.display)) |h| break h;
system.sleep(20);
} else {
log("virtio-gpu: no display service to announce to (scanout-only)\n", .{});
return;
};
var request = dp.Request{
.operation = @intFromEnum(dp.Operation.attach_scanout),
.width = scanout_width,
.height = scanout_height,
.colour = display_format_bgrx,
};
var reply: [dp.reply_size]u8 = undefined;
_ = ipc.callCap(display, std.mem.asBytes(&request), &reply, surface.handle) catch {
log("virtio-gpu: announce to display failed\n", .{});
return;
};
log("virtio-gpu: announced scanout to display\n", .{});
}
/// The `.scanout` service: the compositor asks us to put a composited frame on the panel. The
/// pixels are already in the shared surface, so a present is a transfer-to-host + flush.
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize {
_ = message;
_ = reply;
_ = sender;
_ = capability;
if (message.len < sp.request_size) return 0;
const request = std.mem.bytesToValue(sp.Request, message[0..sp.request_size]);
if (request.operation == @intFromEnum(sp.Operation.present)) {
const presented = presentFull();
const response = sp.Reply{ .status = if (presented) 0 else -1 };
@memcpy(reply[0..sp.reply_size], std.mem.asBytes(&response));
return sp.reply_size;
}
return 0;
}

View File

@ -226,6 +226,7 @@ fn system_call(state: *architecture.CpuState) void {
.wall_clock => systemWallClock(state),
.shm_create => systemShmCreate(state),
.shm_map => systemShmMap(state),
.shm_physical => systemShmPhysical(state),
_ => fail(state),
}
}
@ -536,6 +537,19 @@ fn systemShmMap(state: *architecture.CpuState) void {
architecture.setSystemCallResult(state, base_v);
}
/// shm_physical(cap) -> paddr: the guest-physical base of a shared region the caller holds a
/// capability for. The frames are contiguous (allocated by `allocContiguous`), so a single
/// physical base + length describes the whole region which is exactly what a driver needs
/// to hand a shm surface to a device (virtio-gpu `attach_backing`). Only a holder of the
/// capability can ask; there is no ambient way to turn a virtual address into a physical one.
fn systemShmPhysical(state: *architecture.CpuState) void {
const cap = architecture.systemCallArg(state, 0);
const t = scheduler.current();
if (t.aspace == 0) return fail(state);
const shm = ipc.resolveShm(t, cap) orelse return fail(state); // not an shm handle we hold
architecture.setSystemCallResult(state, shm.phys);
}
/// device_register(parent_id, descriptor_ptr) -> id: publish a child device below a device
/// this process has claimed. The bus-driver primitive: a process that owns a bus
/// enumerates it and hands each device it finds to the table, where a class driver

View File

@ -105,6 +105,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
shmTest(boot_information);
} else if (eql(case, "virtio-gpu")) {
virtioGpuTest(boot_information);
} else if (eql(case, "display-native")) {
displayNativeTest(boot_information);
} else if (eql(case, "clock")) {
clockTest();
} else if (eql(case, "smp")) {
@ -2455,6 +2457,53 @@ fn virtioGpuTest(boot_information: *const BootInformation) void {
while (true) scheduler.yield();
}
/// V4 the native backend + hot-attach (docs/display-v2.md). Boot the compositor and the
/// hardware-free `display-demo` client (as displayDemoTest does), then the device-manager
/// stack so it discovers the virtio-gpu function present via QEMU's `-device
/// virtio-gpu-pci` and spawns the driver. The driver brings up its scanout, then announces
/// the shared surface to the already-running compositor, which maps it, upgrades off the GOP
/// floor, and presents the composited frame through the native backend. Its serial heartbeats
/// `display: scanout upgraded to virtio-gpu` and `display: native present verified` plus
/// the demo's own `display-demo: ok` are the harness's markers. Display is spawned first so
/// it is registered on `.display` before the driver announces.
fn displayNativeTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: display-native\n", .{});
if (boot_information.initial_ramdisk_len == 0) {
check("bootloader handed over an initial_ramdisk", false);
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.setInitialRamdisk(image);
var manager: u32 = 0;
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (!eql(item.name, "device-manager")) continue;
manager = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0;
break;
}
if (manager == 0) {
log("display-native: could not spawn device-manager\n", .{});
result();
return;
}
if (!spawnNamed(rd, "display")) {
log("display-native: could not spawn the display service\n", .{});
result();
return;
}
_ = spawnNamed(rd, "display-demo");
scheduler.setPriority(1); // below the compositor, the demo, and the driver, so they run
while (true) scheduler.yield();
}
/// Process arguments, end to end: spawn args-echo bare (its argv[0] is the
/// initial-ramdisk name). Instance 1 sees argc == 1 and respawns itself through
/// `system_spawn` with the extra arguments "alpha beta-42" the syscall argument

View File

@ -11,6 +11,8 @@ const compositor = @import("compositor.zig");
const system = runtime.system;
const device = runtime.device;
const ipc = runtime.ipc;
const scanout_protocol = runtime.scanout_protocol;
const Rect = compositor.Rect;
const Surface = compositor.Surface;
@ -34,11 +36,23 @@ pub const Gop = struct {
pitch: u32,
format: u32,
fn findDisplay() ?device.DeviceDescriptor {
/// The framebuffer's id and geometry, captured together. `findDisplay` reads these out of
/// the enumeration table and returns them by value, so the caller never re-reads the table
/// across later syscalls (`device_enumerate` writes the whole table straight into this
/// process's memory; reading a descriptor's tail again after other syscalls have run is a
/// window we simply avoid by copying the few fields we need up front).
const Found = struct { id: u64, width: u32, height: u32, pitch: u32, format: u32 };
/// The first `display`-class device with a *valid* (non-zero) geometry, or null. A zero
/// geometry is treated as "not ready yet" so the caller retries a real framebuffer always
/// has a non-zero width, height, and pitch.
fn findDisplay() ?Found {
const total = device.enumerate(&device_table);
const n = @min(total, device_table.len);
for (device_table[0..n]) |d| {
if (d.class == @intFromEnum(device.DeviceClass.display)) return d;
for (device_table[0..n]) |*d| {
if (d.class != @intFromEnum(device.DeviceClass.display)) continue;
if (d.display.width == 0 or d.display.height == 0 or d.display.pitch == 0) continue;
return .{ .id = d.id, .width = d.display.width, .height = d.display.height, .pitch = d.display.pitch, .format = d.display.format };
}
return null;
}
@ -48,7 +62,7 @@ pub const Gop = struct {
pub fn init() ?Gop {
var tries: u32 = 0;
const found = while (tries < 100) : (tries += 1) {
if (findDisplay()) |d| break d;
if (findDisplay()) |f| break f;
system.sleep(50);
} else {
_ = system.write("display: no framebuffer device (headless?)\n");
@ -65,8 +79,7 @@ pub const Gop = struct {
_ = system.write("display: could not map the framebuffer\n");
return null;
};
const geometry = found.display;
const size = @as(usize, geometry.height) * geometry.pitch;
const size = @as(usize, found.height) * found.pitch;
const back_base = system.mmap(size, system.PROT_READ | system.PROT_WRITE);
if (system.mmapFailed(back_base)) {
_ = system.write("display: could not allocate the back buffer\n");
@ -76,10 +89,10 @@ pub const Gop = struct {
.device_id = found.id,
.front = @ptrFromInt(front_base),
.back = @ptrFromInt(back_base),
.width = geometry.width,
.height = geometry.height,
.pitch = geometry.pitch,
.format = geometry.format,
.width = found.width,
.height = found.height,
.pitch = found.pitch,
.format = found.format,
};
}
@ -113,10 +126,48 @@ pub const Gop = struct {
}
};
/// The native virtio-gpu backend: the compositor composes into a **shared** scanout surface
/// (an `shm` region the driver created and handed over) and `present` asks the driver to put
/// a frame on the panel over its `.scanout` endpoint. Unlike GOP there is no local copy the
/// surface *is* the device's resource backing, so compositing writes land straight where the
/// driver transfers-and-flushes from (x86 DMA is cache-coherent, so the cacheable shared pages
/// need no explicit flush). Built by the display service when a driver announces (V4); mode-set
/// and vsync stay off until V5.
pub const VirtioGpu = struct {
pixels: [*]u32, // the shared scanout surface, mapped into the compositor
width: u32,
height: u32,
format: u32,
scanout: ipc.Handle, // the driver's present channel (looked up on `.scanout`)
pub fn info(self: *const VirtioGpu) Info {
return .{ .width = self.width, .height = self.height, .pitch = self.width * 4, .format = self.format };
}
pub fn surface(self: *const VirtioGpu) Surface {
return .{ .pixels = self.pixels, .stride = self.width, .width = self.width, .height = self.height };
}
/// Ask the driver to present. The composited pixels are already in the shared surface, so
/// this is a single request over `.scanout`; the driver transfers + flushes. V4 presents
/// the whole surface (the damage-rect fast path is a later refinement).
pub fn present(self: *const VirtioGpu, damage: Rect) void {
_ = damage;
var request = scanout_protocol.Request{
.operation = @intFromEnum(scanout_protocol.Operation.present),
.x = 0,
.y = 0,
.width = self.width,
.height = self.height,
};
var reply: [scanout_protocol.reply_size]u8 = undefined;
_ = ipc.call(self.scanout, std.mem.asBytes(&request), &reply) catch {};
}
};
/// The pluggable scanout backend. A tagged union so the compositor holds one value and
/// dispatches without caring which is active; a `virtio` variant joins `gop` at V4.
/// dispatches without caring which is active; the `virtio` native backend joins `gop` at V4.
pub const Backend = union(enum) {
gop: Gop,
virtio: VirtioGpu,
pub fn info(self: *const Backend) Info {
return switch (self.*) {
@ -133,16 +184,18 @@ pub const Backend = union(enum) {
inline else => |*b| b.present(damage),
}
}
/// Whether this backend supports runtime mode-setting (GOP: no; a native driver: yes).
/// Whether this backend supports runtime mode-setting (GOP: no; virtio-gpu: not until V5).
pub fn canModeSet(self: *const Backend) bool {
return switch (self.*) {
.gop => false,
.virtio => false,
};
}
/// Whether this backend has a vblank/fence for tear-free present (GOP: no).
/// Whether this backend has a vblank/fence for tear-free present (not until V5).
pub fn hasVsync(self: *const Backend) bool {
return switch (self.*) {
.gop => false,
.virtio => false,
};
}
};

View File

@ -24,10 +24,21 @@ const system = runtime.system;
const Rect = compositor.Rect;
const Surface = compositor.Surface;
/// The active scanout backend GOP today, a native driver when one is present.
/// The active scanout backend the GOP framebuffer at boot, upgraded to a native driver
/// (virtio-gpu) when one announces itself (V4).
var backend: backend_mod.Backend = undefined;
var frames: u64 = 0;
/// This service's endpoint, kept so `attach_scanout` can arm a one-shot timer: the very first
/// native present must happen in a *later* loop iteration, after the reply to the driver's
/// announce has unblocked it and it is serving its `.scanout` channel presenting inline
/// would deadlock (we'd call the driver while it waits on our reply).
var service_endpoint: ipc.Handle = 0;
/// Set when the backend has just been upgraded to virtio-gpu: the next present repaints the
/// whole screen into the shared surface and reads a pixel back to confirm the frame landed.
var pending_native_verify: bool = false;
/// The wallpaper the compositor clears damaged regions to before painting layers.
var background: u32 = 0;
@ -176,6 +187,52 @@ fn present() void {
}
damage = Rect.empty;
frames += 1;
// The first present after a native upgrade confirms the composited frame actually reached
// the shared scanout surface (the automated stand-in for "it's on screen").
if (pending_native_verify and !dirty.isEmpty()) {
pending_native_verify = false;
verifyNativePresent();
}
}
/// Read a pixel straight back from the shared scanout surface after a native present. The
/// surface starts zeroed, so a non-zero centre pixel means the compositor wrote the frame into
/// the pages the driver transfers-and-flushes from that, plus the driver acking the present
/// over `.scanout`, is the serial proof the native path works.
fn verifyNativePresent() void {
const s = backend.surface();
const sample = s.pixels[@as(usize, s.height / 2) * s.stride + s.width / 2];
if (sample != 0) {
_ = system.write("display: native present verified\n");
} else {
_ = system.write("display: native present FAILED (blank surface)\n");
}
}
/// A native scanout driver announced itself: map the shared surface it handed over, find its
/// present channel, switch the backend to virtio-gpu, and queue a full-screen repaint. The
/// present is deferred to a timer (see `service_endpoint`) so it happens after this reply
/// unblocks the driver and it starts serving `.scanout`.
fn attachScanout(width: u32, height: u32, format: u32, capability: ?ipc.Handle, reply: []u8) usize {
const cap = capability orelse return fail(reply);
if (width == 0 or height == 0) return fail(reply);
const mapped = runtime.shm.map(cap) orelse return fail(reply);
const scanout = ipc.lookup(.scanout) orelse return fail(reply);
backend = .{ .virtio = .{
.pixels = @ptrCast(@alignCast(mapped)),
.width = width,
.height = height,
.format = format,
.scanout = scanout,
} };
background = protocol.pack(format, 0x20, 0x30, 0x48); // re-pack the wallpaper for the mode
addDamage(screenRect()); // the whole new surface must be painted
pending_native_verify = true;
_ = system.timerOnce(service_endpoint, 50); // present once the driver is serving .scanout
_ = system.write("display: scanout upgraded to virtio-gpu\n");
return ok(reply);
}
// --- startup self-check -----------------------------------------------------
@ -216,7 +273,7 @@ fn fail_check(_: []const u8) void {
// --- service ----------------------------------------------------------------
fn initialise(endpoint: ipc.Handle) bool {
_ = endpoint;
service_endpoint = endpoint;
// Pick the scanout backend (GOP today). It logs the reason on failure.
backend = backend_mod.select() orelse return false;
@ -254,7 +311,6 @@ fn fail(reply: []u8) usize {
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize {
_ = sender;
_ = capability;
if (message.len < protocol.request_size) return fail(reply);
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
const payload = message[protocol.request_size..];
@ -294,15 +350,26 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Han
present();
return ok(reply);
},
@intFromEnum(protocol.Operation.attach_scanout) => {
return attachScanout(request.width, request.height, request.colour, capability, reply);
},
else => return fail(reply),
}
}
/// The only notification the compositor arms is the post-attach present timer: repaint the
/// screen into the freshly attached native surface and verify the frame landed.
fn onNotification(badge: u64) void {
_ = badge;
present();
}
pub fn main() void {
runtime.service.run(protocol.message_maximum, .{
.service = .display,
.init = initialise,
.on_message = onMessage,
.on_notification = onNotification,
});
}

View File

@ -24,6 +24,11 @@ pub const Operation = enum(u32) {
damage = 6,
/// present(): composite the dirty layers and flush to the screen.
present = 7,
/// attach_scanout(width, height, colour=format) + <surface capability>: a native scanout
/// driver announces itself, handing over the shared scanout surface as an `ipc_call`
/// send_cap. The compositor maps it, looks up the driver's `.scanout` present channel, and
/// upgrades off the GOP floor (docs/display-v2.md V4). `colour` carries the DisplayFormat.
attach_scanout = 8,
};
/// The fixed request header. A `blit_tile`'s pixel payload (width*height 32-bit pixels)

View File

@ -0,0 +1,32 @@
//! The scanout wire protocol what the compositor says to a native scanout driver (e.g.
//! virtio-gpu) over its well-known `.scanout` endpoint to put a composited frame on screen.
//! The driver owns the panel and the shared scanout surface it handed the compositor (via the
//! display service's `attach_scanout`); the compositor composites into that surface, then asks
//! the driver to present a damaged rectangle. Tiny by design one present request. Separate
//! from the display protocol because the directions differ: clients call the compositor over
//! `.display`; the compositor calls the driver over `.scanout`. See docs/display-v2.md.
const std = @import("std");
pub const Operation = enum(u32) {
/// present(x, y, width, height): put the given rectangle of the shared scanout surface on
/// the panel (on virtio-gpu: transfer-to-host of the region, then a resource flush).
present = 0,
};
pub const Request = extern struct {
operation: u32,
x: u32 = 0,
y: u32 = 0,
width: u32 = 0,
height: u32 = 0,
};
pub const Reply = extern struct {
status: i32, // 0 on success, negative on failure
reserved: u32 = 0,
};
pub const message_maximum: usize = 64;
pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);

View File

@ -200,6 +200,17 @@ CASES = [
"qemu_extra": ["-device", "virtio-gpu-pci"],
"expect": r"virtio-gpu: scanout \d+x\d+ online[\s\S]*virtio-gpu: flush acked, pixel check ok",
"fail": r"virtio-gpu:.*(failed|not acked|mismatch|unable to claim|not a virtio-gpu|too small|no PCI capability|does not offer|rejected|missing common-config|not a mapped resource|could not spawn)|CPU EXCEPTION|KERNEL PANIC"},
# Native backend + hot-attach (v2 V4): boot the compositor + display-demo with an emulated
# virtio-gpu. The driver announces its shared scanout surface to the compositor, which maps
# it, upgrades off the GOP floor, and drives frames through the native backend — reading a
# pixel back to confirm the composited frame reached the shared surface, while the demo runs.
{"name": "display-native",
"qemu_extra": ["-device", "virtio-gpu-pci"],
"mem": "512M", # boots the compositor + demo + the whole device-manager driver stack at once
# Order-independent: the demo's `ok` may print before or after the driver announces, so
# require all three markers to appear somewhere rather than in a fixed order.
"expect": r"(?s)(?=.*display: scanout upgraded to virtio-gpu)(?=.*display: native present verified)(?=.*display-demo: ok)",
"fail": r"display: native present FAILED|display: could not|display-demo: (no display|create failed)|CPU EXCEPTION|KERNEL PANIC"},
# Monotonic clock (clock() syscall source): calibrated, advancing, never backwards.
{"name": "clock",
"expect": r"DANOS-TEST-RESULT: PASS",
@ -609,6 +620,8 @@ def run_case(arch, case):
cmd = [arch["qemu"]] + arch["qemu_args"](arch, boot_volume, vars_fd, serial)
if case.get("smp"): # some cases need more than one core (e.g. parallelism)
cmd += ["-smp", str(case["smp"])]
if case.get("mem"): # a case that boots the whole system at once needs more than the 128M floor
cmd[cmd.index("-m") + 1] = case["mem"]
if case.get("qemu_extra"): # extra qemu args, e.g. -device intel-iommu for the IOMMU case
cmd += case["qemu_extra"]
# A QMP control socket, always present (additive): how a case's `qmp_after`