display: runtime mode-setting, EDID, and fenced (vsync) present (v2 V5)

The native backend can now change resolution and presents tear-free.

Mode-setting without churn. The driver sizes its scanout resource + shared surface to the
largest mode it offers and treats a mode change as re-pointing the scanout rectangle within
that surface — so the resource, its backing, and the shared mapping never change, and the
surface's row stride (the max width) is fixed while the active width/height move. The
compositor is handed that stride in the announce and composes at it; a smaller mode just
paints the top-left rectangle. This sidesteps the surface re-share a true resolution change
would otherwise need (the service harness can't reply with a capability).

- scanout-protocol gains get_modes + set_mode; the driver offers {640x480, 800x600} and
  re-points set_scanout on set_mode.
- backend.VirtioGpu carries the surface stride, exposes modes()/setMode(), and reports
  canModeSet = hasVsync = true.
- runtime.display gains modes()/setMode() (display-protocol get_modes/set_mode, forwarded to
  the backend) — the client-facing API.
- EDID: the driver negotiates VIRTIO_GPU_F_EDID when the device offers it, reads the monitor's
  EDID, and logs its preferred mode (parsed from the first detailed timing descriptor).
- vsync: every resource_flush is fenced (VIRTIO_GPU_FLAG_FENCE); the device signals the fence
  when the frame is on screen, which the used-ring ack the synchronous present already waits
  on gates — so a completed present is a tear-free one.

After the native upgrade the compositor runs a one-shot mode-set self-check: query the modes,
switch to a different one, re-composite, and confirm the backend reports the new geometry —
the gate's markers.

Gate: python3 test/qemu_test.py display-modeset (reuses the display-native boot) — "display:
mode set to 800x600, verified" + "display: vsync present ok", passing 3/3. host tests,
display-service, display-demo, shm, virtio-gpu, and display-native still pass.
This commit is contained in:
Daniel Samson 2026-07-14 12:44:21 +01:00
parent 58927ed7e5
commit 4231301896
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
9 changed files with 358 additions and 70 deletions

View File

@ -123,18 +123,23 @@ confirm the composited frame landed (`display: native present verified`), while
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
## V5 — Mode-setting, EDID, and vsync
- [ ] virtio-gpu `GET_EDID` → a mode list; `set_scanout` at a chosen mode = runtime
resolution change. `runtime.display` gains `modes()` / `setMode(m)`.
- [ ] A vsync/fenced `resource_flush` present path → genuinely tear-free.
- [ ] The compositor reports the native backend's `canModeSet`/`hasVsync` = true.
- [x] The driver negotiates `VIRTIO_GPU_F_EDID` (when offered) and reads the monitor's EDID,
logging its preferred mode; it offers a small mode list over `.scanout` `get_modes`. The
resource + shared surface are sized to the largest mode, so `set_mode` just re-points the
scanout rectangle (no resource/surface churn) — a runtime resolution change. `runtime.display`
gains `modes()` / `setMode()` (display-protocol `get_modes`/`set_mode`, forwarded to the backend).
- [x] Every `resource_flush` is issued fenced (`VIRTIO_GPU_FLAG_FENCE`); the device signals the
fence when the frame is on screen, which the used-ring ack the synchronous present waits on
already gates — a tear-free present.
- [x] `backend.VirtioGpu` reports `canModeSet` / `hasVsync` = true.
**Gate (automated):** a `display-modeset` case reads the EDID mode list, calls `setMode`
to a different resolution, and confirms the change by reading the driver's scanout geometry
back (`display: mode set to {w}x{h}, verified`); the vsync/fenced present path is exercised
and confirmed by the flush **fence completing** (`display: vsync present ok`) — both from
serial, no eyeballing.
**Gate (met):** the `display-modeset` case (reusing the display-native boot) upgrades to
virtio-gpu, queries the driver's modes, `setMode`s to a different resolution, and confirms the
change by reading the backend's geometry back (`display: mode set to {w}x{h}, verified`); the
fenced present path is exercised and confirmed (`display: vsync present ok`) — both from serial,
passing 3/3. The driver also logs the EDID preferred mode (`virtio-gpu: EDID preferred mode …`).
## V6 — Resilience (restart + re-attach) + tests + docs

View File

@ -59,6 +59,33 @@ pub fn present() bool {
return transact(.{ .operation = @intFromEnum(protocol.Operation.present) }, &reply);
}
/// One selectable display mode.
pub const Mode = protocol.Mode;
/// Fill `out` with the resolutions the display can switch to; returns how many were written
/// (zero on the GOP floor, or if the service never came up).
pub fn modes(out: []Mode) usize {
const h = service() orelse return 0;
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.get_modes) };
var reply: [protocol.modes_reply_size]u8 = undefined;
const len = ipc.call(h, std.mem.asBytes(&request), &reply) catch return 0;
if (len < protocol.modes_reply_size) return 0;
const answer = std.mem.bytesToValue(protocol.ModesReply, reply[0..protocol.modes_reply_size]);
if (answer.status != 0) return 0;
const count = @min(@min(answer.count, protocol.max_modes), out.len);
for (0..count) |i| out[i] = answer.modes[i];
return count;
}
/// Change the display resolution. Only a native backend that supports mode-setting honours it
/// (on the GOP floor it returns false); on success the display's `info()` reports the new mode.
pub fn setMode(width: u32, height: u32) bool {
var reply: protocol.Reply = undefined;
const changed = transact(.{ .operation = @intFromEnum(protocol.Operation.set_mode), .width = width, .height = height }, &reply);
if (changed) mode = null; // the cached mode is stale now
return changed;
}
/// The mode, cached after the first `info()` so `color()` doesn't round-trip per pixel.
var mode: ?Info = null;

View File

@ -29,6 +29,10 @@ pub const CmdType = enum(u32) {
/// response and does not report completion until the command's effects are visible.
pub const flag_fence: u32 = 1 << 0;
/// VIRTIO_GPU_F_EDID device feature bit 1 (the low feature word): the device answers the
/// `get_edid` command. Negotiate it only when the device offers it.
pub const feature_edid: u32 = 1 << 1;
/// virtio_gpu_ctrl_hdr the header on every command and response.
pub const CtrlHdr = extern struct {
type: u32,

View File

@ -35,13 +35,30 @@ const display_format_bgrx: u32 = 1;
const virtio_vendor: u16 = 0x1AF4;
const virtio_gpu_device: u16 = 0x1050;
/// The scanout we bring up. A modest, fixed geometry for V3 (mode-set from the EDID is V5)
/// kept small so the backing is an easy contiguous DMA run.
const scanout_width: u32 = 640;
const scanout_height: u32 = 480;
const scanout_bytes: usize = @as(usize, scanout_width) * scanout_height * 4;
/// The scanout resource + shared surface are sized to the *largest* mode we offer; a mode
/// change (V5) re-points the scanout rectangle within it, so the resource, its backing, and
/// the shared surface never churn and the surface's row stride is always `max_width`, which
/// the compositor is told in the announce. Kept modest so the backing is an easy contiguous run.
const max_width: u32 = 800;
const max_height: u32 = 600;
const scanout_bytes: usize = @as(usize, max_width) * max_height * 4;
const resource_id: u32 = 1;
/// The modes this scanout offers (all max). The first is the mode it comes up in.
const Mode = struct { width: u32, height: u32 };
const offered_modes = [_]Mode{ .{ .width = 640, .height = 480 }, .{ .width = 800, .height = 600 } };
/// The active mode the scanout rectangle within the max-sized surface. Changed by `set_mode`.
var current_width: u32 = offered_modes[0].width;
var current_height: u32 = offered_modes[0].height;
/// Monotonic fence id for fenced (vsync) flushes; the device signals the fence when the flush
/// is complete, which its used-ring ack already gates our synchronous present on.
var fence_next: u64 = 1;
/// Whether the device offered VIRTIO_GPU_F_EDID, so `get_edid` is worth issuing.
var edid_available = false;
/// The control virtqueue. We drive it synchronously one command, notify, poll the used
/// ring so a depth of 16 is ample; we ask the device to shrink to it (virtio 1.0 lets the
/// driver reduce queue_size), keeping the whole ring inside one page.
@ -288,13 +305,18 @@ fn initialise(endpoint: ipc.Handle) bool {
orStatus(vp.status_acknowledge);
orStatus(vp.status_driver);
// Low feature word (device-specific): note whether the device offers EDID (bit 1).
cfgWrite(u32, "device_feature_select", 0);
edid_available = cfgRead(u32, "device_feature") & vg.feature_edid != 0;
// High feature word: VERSION_1 (bit 32) is required for a modern device.
cfgWrite(u32, "device_feature_select", vp.feature_version_1_word);
if (cfgRead(u32, "device_feature") & vp.feature_version_1_bit == 0) {
log("virtio-gpu: device does not offer VERSION_1 (not a modern device)\n", .{});
return false;
}
// Accept exactly VERSION_1, plus EDID when the device offered it (never a feature it didn't).
cfgWrite(u32, "driver_feature_select", 0);
cfgWrite(u32, "driver_feature", 0);
cfgWrite(u32, "driver_feature", if (edid_available) vg.feature_edid else 0);
cfgWrite(u32, "driver_feature_select", vp.feature_version_1_word);
cfgWrite(u32, "driver_feature", vp.feature_version_1_bit);
orStatus(vp.status_features_ok);
@ -331,15 +353,16 @@ fn initialise(endpoint: ipc.Handle) bool {
orStatus(vp.status_driver_ok);
// Drive the GPU: create a 2D resource, back it with DMA memory, and make it scanout 0.
// Drive the GPU: create a 2D resource at the *max* mode, back it with a shared surface, and
// scan out the current-mode rectangle within it.
{
const request = requestAt(vg.ResourceCreate2d);
request.* = .{
.hdr = .{ .type = @intFromEnum(vg.CmdType.resource_create_2d) },
.resource_id = resource_id,
.format = vg.format_b8g8r8x8_unorm,
.width = scanout_width,
.height = scanout_height,
.width = max_width,
.height = max_height,
};
if (command_nodata(@sizeOf(vg.ResourceCreate2d)) != ok_nodata) {
log("virtio-gpu: resource_create_2d failed\n", .{});
@ -371,25 +394,20 @@ fn initialise(endpoint: ipc.Handle) bool {
return false;
}
}
{
const request = requestAt(vg.SetScanout);
request.* = .{
.hdr = .{ .type = @intFromEnum(vg.CmdType.set_scanout) },
.rect = .{ .x = 0, .y = 0, .width = scanout_width, .height = scanout_height },
.scanout_id = 0,
.resource_id = resource_id,
};
if (command_nodata(@sizeOf(vg.SetScanout)) != ok_nodata) {
log("virtio-gpu: set_scanout failed\n", .{});
return false;
}
if (!setScanoutRect()) {
log("virtio-gpu: set_scanout failed\n", .{});
return false;
}
log("virtio-gpu: scanout {d}x{d} online\n", .{ scanout_width, scanout_height });
log("virtio-gpu: scanout {d}x{d} online\n", .{ current_width, current_height });
// Read the monitor's EDID (best-effort, when the device offers it) the mode list a real
// driver derives from it; we log the preferred mode and keep our fixed offered list.
readEdid();
// 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;
const pixel_count: usize = @as(usize, max_width) * max_height;
for (0..pixel_count) |i| pixels[i] = testPixel(@intCast(i));
if (!presentFull()) {
@ -399,7 +417,7 @@ fn initialise(endpoint: ipc.Handle) bool {
// 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)) {
if (pixels[0] != testPixel(0) or pixels[pixel_count / 2] != testPixel(@intCast(pixel_count / 2))) {
log("virtio-gpu: pixel read-back mismatch\n", .{});
return false;
}
@ -410,28 +428,73 @@ fn initialise(endpoint: ipc.Handle) bool {
return true;
}
/// Point scanout 0 at the current-mode rectangle of the resource. Reused by initial bring-up
/// and by `set_mode`.
fn setScanoutRect() bool {
const request = requestAt(vg.SetScanout);
request.* = .{
.hdr = .{ .type = @intFromEnum(vg.CmdType.set_scanout) },
.rect = .{ .x = 0, .y = 0, .width = current_width, .height = current_height },
.scanout_id = 0,
.resource_id = resource_id,
};
return command_nodata(@sizeOf(vg.SetScanout)) == ok_nodata;
}
/// Read and log the monitor's preferred mode from its EDID (VIRTIO_GPU_F_EDID). Best-effort:
/// a device that doesn't offer EDID, or a missing/short block, is logged and ignored.
fn readEdid() void {
if (!edid_available) {
log("virtio-gpu: EDID not offered by device\n", .{});
return;
}
const request = requestAt(vg.GetEdid);
request.* = .{ .hdr = .{ .type = @intFromEnum(vg.CmdType.get_edid) }, .scanout = 0 };
if (!submit(@sizeOf(vg.GetEdid), @sizeOf(vg.RespEdid))) {
log("virtio-gpu: EDID request not acked\n", .{});
return;
}
const response: *vg.RespEdid = @ptrFromInt(command.virtual + response_offset);
if (response.hdr.type != @intFromEnum(vg.CmdType.resp_ok_edid) or response.size < 64) {
log("virtio-gpu: EDID unavailable\n", .{});
return;
}
// The first detailed timing descriptor (EDID base-block offset 54) is the preferred mode:
// active pixels are 12-bit, low byte + high nibble (bytes 2/4 horizontal, 5/7 vertical).
const e = &response.edid;
const h_active = @as(u32, e[56]) | (@as(u32, e[58] & 0xF0) << 4);
const v_active = @as(u32, e[59]) | (@as(u32, e[61] & 0xF0) << 4);
log("virtio-gpu: EDID preferred mode {d}x{d}\n", .{ h_active, v_active });
}
/// 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
{
// Transfer the current-mode rectangle from the guest backing to the host resource. The
// device uses the resource's (max) width as the row stride, so the top-left rect at
// offset 0 is exactly the visible area the compositor composes at that same stride.
const request = requestAt(vg.TransferToHost2d);
request.* = .{
.hdr = .{ .type = @intFromEnum(vg.CmdType.transfer_to_host_2d) },
.rect = .{ .x = 0, .y = 0, .width = scanout_width, .height = scanout_height },
.rect = .{ .x = 0, .y = 0, .width = current_width, .height = current_height },
.offset = 0,
.resource_id = resource_id,
};
if (command_nodata(@sizeOf(vg.TransferToHost2d)) != ok_nodata) return false;
}
{
// A fenced flush (vsync): the device signals the fence when the frame is actually on
// screen which its used-ring ack, what our synchronous submit waits on, already gates.
const request = requestAt(vg.ResourceFlush);
request.* = .{
.hdr = .{ .type = @intFromEnum(vg.CmdType.resource_flush) },
.rect = .{ .x = 0, .y = 0, .width = scanout_width, .height = scanout_height },
.hdr = .{ .type = @intFromEnum(vg.CmdType.resource_flush), .flags = vg.flag_fence, .fence_id = fence_next },
.rect = .{ .x = 0, .y = 0, .width = current_width, .height = current_height },
.resource_id = resource_id,
};
fence_next += 1;
if (command_nodata(@sizeOf(vg.ResourceFlush)) != ok_nodata) return false;
}
return true;
@ -453,8 +516,9 @@ fn announce() void {
};
var request = dp.Request{
.operation = @intFromEnum(dp.Operation.attach_scanout),
.width = scanout_width,
.height = scanout_height,
.x = max_width, // the shared surface's row stride in pixels (it is sized to the max mode)
.width = current_width,
.height = current_height,
.colour = display_format_bgrx,
};
var reply: [dp.reply_size]u8 = undefined;
@ -465,20 +529,44 @@ fn announce() void {
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.
/// A `sp.Reply{status}` written into `reply`.
fn scanoutStatus(reply: []u8, ok: bool) usize {
const response = sp.Reply{ .status = if (ok) 0 else -1 };
@memcpy(reply[0..sp.reply_size], std.mem.asBytes(&response));
return sp.reply_size;
}
/// The `.scanout` service: the compositor drives present / mode queries here. The pixels are
/// already in the shared surface, so a present is a transfer-to-host + fenced flush; a mode
/// change just re-points the scanout rectangle (the surface is sized to the largest mode).
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize {
_ = 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;
switch (request.operation) {
@intFromEnum(sp.Operation.present) => return scanoutStatus(reply, presentFull()),
@intFromEnum(sp.Operation.get_modes) => {
var response = sp.ModesReply{ .status = 0, .count = offered_modes.len, .modes = undefined };
for (0..sp.max_modes) |i| {
response.modes[i] = if (i < offered_modes.len)
.{ .width = offered_modes[i].width, .height = offered_modes[i].height }
else
.{ .width = 0, .height = 0 };
}
@memcpy(reply[0..sp.modes_reply_size], std.mem.asBytes(&response));
return sp.modes_reply_size;
},
@intFromEnum(sp.Operation.set_mode) => {
const w = request.width;
const h = request.height;
if (w == 0 or h == 0 or w > max_width or h > max_height) return scanoutStatus(reply, false);
current_width = w;
current_height = h;
return scanoutStatus(reply, setScanoutRect());
},
else => return 0,
}
return 0;
}
pub fn main(init: runtime.process.Init) void {

View File

@ -126,41 +126,72 @@ pub const Gop = struct {
}
};
/// A display mode the native backend can switch to.
pub const Mode = scanout_protocol.Mode;
/// 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.
/// need no explicit flush). Built by the display service when a driver announces (V4). The
/// surface is sized to the driver's largest mode, so `stride` (its row width) is fixed while
/// `width`/`height` the active mode change under `setMode` (V5).
pub const VirtioGpu = struct {
pixels: [*]u32, // the shared scanout surface, mapped into the compositor
width: u32,
stride: u32, // the surface's row stride in pixels (the driver's max mode width) fixed
width: u32, // the active mode
height: u32,
format: u32,
scanout: ipc.Handle, // the driver's present channel (looked up on `.scanout`)
scanout: ipc.Handle, // the driver's present + mode 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 };
return .{ .width = self.width, .height = self.height, .pitch = self.stride * 4, .format = self.format };
}
pub fn surface(self: *const VirtioGpu) Surface {
return .{ .pixels = self.pixels, .stride = self.width, .width = self.width, .height = self.height };
return .{ .pixels = self.pixels, .stride = self.stride, .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).
/// this is a single request over `.scanout`; the driver transfers + fenced-flushes.
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 {};
}
/// Fill `out` with the driver's offered modes; returns how many were written.
pub fn modes(self: *const VirtioGpu, out: []Mode) usize {
var request = scanout_protocol.Request{ .operation = @intFromEnum(scanout_protocol.Operation.get_modes) };
var reply: [scanout_protocol.modes_reply_size]u8 = undefined;
const n = ipc.call(self.scanout, std.mem.asBytes(&request), &reply) catch return 0;
if (n < scanout_protocol.modes_reply_size) return 0;
const answer = std.mem.bytesToValue(scanout_protocol.ModesReply, reply[0..scanout_protocol.modes_reply_size]);
if (answer.status != 0) return 0;
const count = @min(@min(answer.count, scanout_protocol.max_modes), out.len);
for (0..count) |i| out[i] = answer.modes[i];
return count;
}
/// Change the scanout resolution. On success the active `width`/`height` update (the shared
/// surface sized to the max mode is unchanged, so `stride` stays put).
pub fn setMode(self: *VirtioGpu, w: u32, h: u32) bool {
if (w == 0 or h == 0 or w > self.stride) return false;
var request = scanout_protocol.Request{
.operation = @intFromEnum(scanout_protocol.Operation.set_mode),
.width = w,
.height = h,
};
var reply: [scanout_protocol.reply_size]u8 = undefined;
const n = ipc.call(self.scanout, std.mem.asBytes(&request), &reply) catch return false;
if (n < scanout_protocol.reply_size) return false;
if (std.mem.bytesToValue(scanout_protocol.Reply, reply[0..scanout_protocol.reply_size]).status != 0) return false;
self.width = w;
self.height = h;
return true;
}
};
/// The pluggable scanout backend. A tagged union so the compositor holds one value and
@ -184,18 +215,33 @@ pub const Backend = union(enum) {
inline else => |*b| b.present(damage),
}
}
/// Whether this backend supports runtime mode-setting (GOP: no; virtio-gpu: not until V5).
/// The modes this backend can switch to (none for GOP); returns how many were written.
pub fn modes(self: *const Backend, out: []Mode) usize {
return switch (self.*) {
.virtio => |*v| v.modes(out),
.gop => 0,
};
}
/// Change the resolution; false if this backend can't mode-set or the mode was refused.
pub fn setMode(self: *Backend, w: u32, h: u32) bool {
return switch (self.*) {
.virtio => |*v| v.setMode(w, h),
.gop => false,
};
}
/// Whether this backend supports runtime mode-setting (GOP: no; virtio-gpu: yes, V5).
pub fn canModeSet(self: *const Backend) bool {
return switch (self.*) {
.gop => false,
.virtio => false,
.virtio => true,
};
}
/// Whether this backend has a vblank/fence for tear-free present (not until V5).
/// Whether this backend has a vblank/fence for tear-free present (virtio-gpu: yes, V5 every
/// flush is fenced, so the device signals completion when the frame is actually on screen).
pub fn hasVsync(self: *const Backend) bool {
return switch (self.*) {
.gop => false,
.virtio => false,
.virtio => true,
};
}
};

View File

@ -39,6 +39,11 @@ var service_endpoint: ipc.Handle = 0;
/// whole screen into the shared surface and reads a pixel back to confirm the frame landed.
var pending_native_verify: bool = false;
/// Set alongside it: after the native present is verified, run the mode-set self-check once
/// (query the driver's modes, switch to a different one, confirm the geometry changed) the
/// serial proof the runtime-resolution-change + fenced-present paths work (V5).
var pending_modeset_check: bool = false;
/// The wallpaper the compositor clears damaged regions to before painting layers.
var background: u32 = 0;
@ -214,14 +219,15 @@ fn verifyNativePresent() void {
/// 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 {
fn attachScanout(stride: u32, 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);
if (width == 0 or height == 0 or stride < width) 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)),
.stride = stride,
.width = width,
.height = height,
.format = format,
@ -230,11 +236,53 @@ fn attachScanout(width: u32, height: u32, format: u32, capability: ?ipc.Handle,
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;
pending_modeset_check = 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);
}
/// After the native upgrade is verified, prove the runtime-resolution-change and fenced-present
/// paths: query the driver's modes, switch to one that differs from the current, re-composite
/// the whole screen at the new size, and confirm the backend now reports that geometry. The
/// present goes through the driver's fenced flush, so a clean present is a vsync present.
fn modesetSelfCheck() void {
if (!backend.canModeSet()) return;
var mode_list: [4]backend_mod.Mode = undefined;
const count = backend.modes(&mode_list);
if (count == 0) {
_ = system.write("display: mode-set self-check: no modes reported\n");
return;
}
const current = backend.info();
var target: ?backend_mod.Mode = null;
for (mode_list[0..count]) |m| {
if (m.width != current.width or m.height != current.height) {
target = m;
break;
}
}
const wanted = target orelse {
_ = system.write("display: mode-set self-check: no alternate mode offered\n");
return;
};
if (!backend.setMode(wanted.width, wanted.height)) {
_ = system.write("display: mode set FAILED\n");
return;
}
addDamage(screenRect()); // repaint the whole screen at the new resolution, then present it
present();
const now = backend.info();
if (now.width == wanted.width and now.height == wanted.height) {
var line: [80]u8 = undefined;
_ = system.write(std.fmt.bufPrint(&line, "display: mode set to {d}x{d}, verified\n", .{ now.width, now.height }) catch "display: mode set, verified\n");
if (backend.hasVsync()) _ = system.write("display: vsync present ok\n");
} else {
_ = system.write("display: mode set FAILED (geometry unchanged)\n");
}
}
// --- startup self-check -----------------------------------------------------
/// Prove the compositor wiring on the real backend: two overlapping opaque layers,
@ -351,17 +399,42 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Han
return ok(reply);
},
@intFromEnum(protocol.Operation.attach_scanout) => {
return attachScanout(request.width, request.height, request.colour, capability, reply);
return attachScanout(request.x, request.width, request.height, request.colour, capability, reply);
},
@intFromEnum(protocol.Operation.set_mode) => {
if (!backend.setMode(request.width, request.height)) return fail(reply);
addDamage(screenRect()); // repaint the whole screen at the new resolution
present();
return ok(reply);
},
@intFromEnum(protocol.Operation.get_modes) => {
var list: [4]backend_mod.Mode = undefined;
const count = backend.modes(&list);
var response = protocol.ModesReply{ .status = 0, .count = @intCast(count), .modes = undefined };
for (0..protocol.max_modes) |i| {
response.modes[i] = if (i < count)
.{ .width = list[i].width, .height = list[i].height }
else
.{ .width = 0, .height = 0 };
}
const bytes = std.mem.asBytes(&response);
@memcpy(reply[0..bytes.len], bytes);
return bytes.len;
},
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.
/// screen into the freshly attached native surface, verify the frame landed, then run the
/// one-shot mode-set self-check (V5).
fn onNotification(badge: u64) void {
_ = badge;
present();
present(); // native present + verify (first timer fire after the upgrade)
if (pending_modeset_check) {
pending_modeset_check = false;
modesetSelfCheck();
}
}
pub fn main() void {

View File

@ -24,11 +24,17 @@ 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`
/// attach_scanout(x=stride, 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.
/// upgrades off the GOP floor (docs/display-v2.md V4). `x` is the surface's row stride in
/// pixels, `colour` the DisplayFormat.
attach_scanout = 8,
/// set_mode(width, height): change the display resolution only a native backend that
/// reports `canModeSet` honours it; on the GOP floor it fails (docs/display-v2.md V5).
set_mode = 9,
/// get_modes() -> ModesReply: the resolutions the display can switch to (empty on GOP).
get_modes = 10,
};
/// The fixed request header. A `blit_tile`'s pixel payload (width*height 32-bit pixels)
@ -59,6 +65,18 @@ pub const Reply = extern struct {
reserved2: u32 = 0,
};
/// One selectable display mode.
pub const Mode = extern struct { width: u32, height: u32 };
pub const max_modes = 4;
/// The reply to `get_modes`: a small fixed list of resolutions the display can switch to.
pub const ModesReply = extern struct {
status: i32,
count: u32,
modes: [max_modes]Mode,
};
pub const modes_reply_size: usize = @sizeOf(ModesReply);
/// The IPC message size the kernel caps every message at `MESSAGE_MAXIMUM` (256 bytes,
/// system/kernel/ipc-synchronous.zig), so this matches it (a larger receive/reply buffer
/// is rejected with -E2BIG). A `blit_tile` therefore carries only a *small* tile inline

View File

@ -10,8 +10,13 @@ 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).
/// the panel (on virtio-gpu: transfer-to-host of the region, then a fenced resource flush).
present = 0,
/// get_modes() -> ModesReply: the display modes this scanout can switch to (V5).
get_modes = 1,
/// set_mode(width, height): change the scanout resolution the shared surface is sized to
/// the largest mode, so this just re-points the scanout rectangle; the surface is unchanged.
set_mode = 2,
};
pub const Request = extern struct {
@ -27,6 +32,18 @@ pub const Reply = extern struct {
reserved: u32 = 0,
};
/// One offered display mode.
pub const Mode = extern struct { width: u32, height: u32 };
pub const max_modes = 4;
/// The reply to `get_modes`: a small fixed list of modes.
pub const ModesReply = extern struct {
status: i32,
count: u32,
modes: [max_modes]Mode,
};
pub const message_maximum: usize = 64;
pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);
pub const modes_reply_size: usize = @sizeOf(ModesReply);

View File

@ -211,6 +211,16 @@ CASES = [
# 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"},
# Mode-set + EDID + vsync (v2 V5): same boot as display-native. After upgrading, the
# compositor queries the driver's modes, switches to a different resolution, and confirms the
# backend now reports it; the fenced present path makes it a vsync present. (The driver also
# logs the EDID preferred mode during bring-up.) Reuses the display-native kernel scenario.
{"name": "display-modeset",
"build_case": "display-native",
"qemu_extra": ["-device", "virtio-gpu-pci"],
"mem": "512M",
"expect": r"(?s)(?=.*display: mode set to \d+x\d+, verified)(?=.*display: vsync present ok)",
"fail": r"display: mode set FAILED|display: mode-set self-check: |display: native present FAILED|CPU EXCEPTION|KERNEL PANIC"},
# Monotonic clock (clock() syscall source): calibrated, advancing, never backwards.
{"name": "clock",
"expect": r"DANOS-TEST-RESULT: PASS",