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:
@@ -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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user