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