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
13 changed files with 379 additions and 58 deletions
+70 -3
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,
});
}