library: five protocols speak the envelope
The folded header stops being a rule in a document and becomes the layout on the wire. Verbs number from sixteen, leaving describe, enumerate, subscribe and unsubscribe reserved and answered the same way by every provider — none of them writes a line to do it. What each protocol used to carry in a field of its own now travels in the header: a vfs node and a display layer are the packet's target, and a reply opens with a status the envelope stamps rather than one each protocol spelled for itself. Display gains the most. One forty-byte request had served eleven verbs, so attach_scanout smuggled stride through x, refresh through y and format through colour, and every coordinate crossed as a bitcast. Per-operation structs end all three: the fields have their own names and their own signs, and the tile payload grows to 224 bytes because the prefix shrank. Scanout loses a message maximum of 64 it had no business declaring — it answers calls, and the floor for a call is 256 — and virtio-gpu stops hard-coding that number at its harness. Two changes are semantic rather than notational. A directory now ends at an entry with no name, because the fixed part of a reply always travels and a zero-length reply no longer exists to mean anything. And input joins the service harness, the last loop in the tree that answered no ping and heard no terminate; its subscriber table, its pruning and its fan-out are the same code, and a shutdown now asks it to stop instead of killing it. A new conformance case reads the registry's own listing and asks every protocol it finds for its name, its version and its verb count, then offers a verb nobody defines and requires -ENOSYS — the envelope's promise, checked against providers rather than against itself. What it cannot reach in that boot it names on the serial line instead of passing quietly. Suite 110/110.
This commit is contained in:
@@ -13,6 +13,7 @@ const memory = @import("memory");
|
||||
const logging = @import("logging");
|
||||
const compositor = @import("compositor.zig");
|
||||
|
||||
const envelope = @import("envelope");
|
||||
const scanout_protocol = @import("scanout-protocol");
|
||||
const Rect = compositor.Rect;
|
||||
const Surface = compositor.Surface;
|
||||
@@ -188,45 +189,53 @@ pub const VirtioGpu = struct {
|
||||
/// accumulated; the driver transfers + fenced-flushes the whole frame.
|
||||
pub fn present(self: *const VirtioGpu, damage: []const Rect) void {
|
||||
_ = damage;
|
||||
var request = scanout_protocol.Request{
|
||||
.operation = @intFromEnum(scanout_protocol.Operation.present),
|
||||
.width = self.width,
|
||||
.height = self.height,
|
||||
};
|
||||
var reply: [scanout_protocol.reply_size]u8 = undefined;
|
||||
_ = ipc.call(self.scanout, std.mem.asBytes(&request), &reply) catch {};
|
||||
_ = call(self.scanout, .present, .{ .width = self.width, .height = self.height });
|
||||
}
|
||||
/// 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];
|
||||
const answered = call(self.scanout, .get_modes, {}) orelse return 0;
|
||||
const offered = Scanout.decodeReply(.get_modes, answered.packet[0..answered.len]) orelse return 0;
|
||||
const count = @min(@min(offered.count, scanout_protocol.max_modes), out.len);
|
||||
for (0..count) |i| out[i] = offered.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;
|
||||
_ = call(self.scanout, .set_mode, .{ .width = w, .height = h }) orelse return false;
|
||||
self.width = w;
|
||||
self.height = h;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const Scanout = scanout_protocol.Protocol;
|
||||
|
||||
/// A reply the driver answered with, kept whole so the caller can decode the
|
||||
/// verb's own fixed part out of it.
|
||||
const Answered = struct {
|
||||
packet: [scanout_protocol.message_maximum]u8,
|
||||
len: usize,
|
||||
};
|
||||
|
||||
/// One request at the scanout driver. Null covers both a transport failure and a
|
||||
/// driver that refused — a present that did not happen is a present that did not
|
||||
/// happen, and this backend has nothing to do about either but skip the frame.
|
||||
fn call(
|
||||
scanout: ipc.Handle,
|
||||
comptime operation: Scanout.Operation,
|
||||
request: Scanout.RequestOf(operation),
|
||||
) ?Answered {
|
||||
var packet: [scanout_protocol.message_maximum]u8 = undefined;
|
||||
const framed = Scanout.encodeRequest(operation, 0, request, &.{}, &packet) orelse return null;
|
||||
var answered: Answered = .{ .packet = undefined, .len = 0 };
|
||||
answered.len = ipc.call(scanout, framed, &answered.packet) catch return null;
|
||||
const status = envelope.statusOf(answered.packet[0..answered.len]) orelse return null;
|
||||
if (status.status != 0) return null;
|
||||
return answered;
|
||||
}
|
||||
|
||||
/// The pluggable scanout backend. A tagged union so the compositor holds one value and
|
||||
/// dispatches without caring which is active; the `virtio` native backend joins `gop` at V4.
|
||||
pub const Backend = union(enum) {
|
||||
|
||||
@@ -10,8 +10,9 @@ pub fn build(b: *std.Build) void {
|
||||
.name = "display",
|
||||
.root_source_file = b.path("display.zig"),
|
||||
.imports = &.{
|
||||
"channel", "display-client", "display-protocol", "driver", "input-client", "ipc",
|
||||
"logging", "memory", "scanout-protocol", "service", "thread", "time",
|
||||
"channel", "display-client", "display-protocol", "driver", "envelope", "input-client",
|
||||
"ipc", "logging", "memory", "scanout-protocol", "service",
|
||||
"thread", "time",
|
||||
},
|
||||
.threaded = true, // real atomics/TLS (docs/threading.md)
|
||||
});
|
||||
|
||||
@@ -28,10 +28,22 @@ const logging = @import("logging");
|
||||
const compositor = @import("compositor.zig");
|
||||
const backend_mod = @import("backend.zig");
|
||||
|
||||
const envelope = @import("envelope");
|
||||
const display_protocol = @import("display-protocol");
|
||||
const Rect = compositor.Rect;
|
||||
const Surface = compositor.Surface;
|
||||
|
||||
/// The generated display dispatch. One compositor per process, so the handler
|
||||
/// context is empty and the layer stack stays in this file's globals.
|
||||
const Serve = display_protocol.Protocol.Provider(void);
|
||||
|
||||
const Invocation = envelope.Invocation;
|
||||
const Answer = envelope.Answer;
|
||||
|
||||
/// What a handler returns when the layer named in `Header.target` is not one of
|
||||
/// ours, or a mode was refused.
|
||||
const refused: isize = -envelope.ENOENT;
|
||||
|
||||
/// 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;
|
||||
@@ -314,11 +326,18 @@ fn verifyNativePresent() void {
|
||||
/// `systemSharedMemoryMap`) — the pixels stay ours after the handle naming them
|
||||
/// goes, and a driver that dies and re-announces no longer costs a handle slot
|
||||
/// per restart.
|
||||
fn attachScanout(stride: u32, width: u32, height: u32, format: u32, refresh_hz: u32, arrived: *ipc.Arrival, reply: []u8) usize {
|
||||
const cap = arrived.peek() orelse return fail(reply);
|
||||
if (width == 0 or height == 0 or stride < width) return fail(reply);
|
||||
const mapped = memory.sharedMap(cap) orelse return fail(reply);
|
||||
const scanout = channel.openEndpoint("scanout") orelse return fail(reply);
|
||||
fn onAttachScanout(_: void, invocation: Invocation(display_protocol.AttachScanout), _: Answer(void)) isize {
|
||||
const announce = invocation.request;
|
||||
const stride = announce.stride;
|
||||
const width = announce.width;
|
||||
const height = announce.height;
|
||||
const format = announce.format;
|
||||
const refresh_hz = announce.refresh_hz;
|
||||
|
||||
const cap = invocation.capability orelse return refused;
|
||||
if (width == 0 or height == 0 or stride < width) return refused;
|
||||
const mapped = memory.sharedMap(cap) orelse return refused;
|
||||
const scanout = channel.openEndpoint("scanout") orelse return refused;
|
||||
// A second announce means the driver died and was restarted (V6): re-attach to its fresh
|
||||
// scanout. (The previous shared mapping leaks — there is no shared_memory_unmap syscall yet — but the
|
||||
// frames are the dead driver's, reclaimed on its exit; a handful across a crash is benign.)
|
||||
@@ -346,7 +365,7 @@ fn attachScanout(stride: u32, width: u32, height: u32, format: u32, refresh_hz:
|
||||
"display: scanout re-attached\n"
|
||||
else
|
||||
"display: scanout upgraded to virtio-gpu\n");
|
||||
return ok(reply);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// After the native upgrade is verified, prove the runtime-resolution-change and fenced-present
|
||||
@@ -609,88 +628,109 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
fn writeReply(reply: []u8, value: display_protocol.Reply) usize {
|
||||
const bytes = std.mem.asBytes(&value);
|
||||
@memcpy(reply[0..bytes.len], bytes);
|
||||
return bytes.len;
|
||||
// --- the protocol handlers --------------------------------------------------
|
||||
//
|
||||
// A layer id is `Header.target` on every verb that names one, so no handler
|
||||
// reads a layer out of its own request any more. `target` is a u64 and a layer
|
||||
// id a u32: a value that does not fit is not a layer of ours, and `layerAt`
|
||||
// refuses it the same way an out-of-range one is refused.
|
||||
|
||||
fn targetLayer(target: u64) ?u32 {
|
||||
if (target > std.math.maxInt(u32)) return null;
|
||||
return @intCast(target);
|
||||
}
|
||||
|
||||
fn ok(reply: []u8) usize {
|
||||
return writeReply(reply, .{ .status = 0 });
|
||||
fn onInfo(_: void, _: Invocation(void), answer: Answer(display_protocol.Info)) isize {
|
||||
const mode = backend.info();
|
||||
answer.set(.{ .width = mode.width, .height = mode.height, .pitch = mode.pitch, .format = mode.format });
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn fail(reply: []u8) usize {
|
||||
return writeReply(reply, .{ .status = -1 });
|
||||
fn onCreateLayer(_: void, invocation: Invocation(display_protocol.CreateLayer), answer: Answer(display_protocol.Created)) isize {
|
||||
const request = invocation.request;
|
||||
const slot = createLayer(request.x, request.y, request.width, request.height, request.z, request.visible != 0) orelse return refused;
|
||||
answer.set(.{ .layer = slot });
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn onConfigureLayer(_: void, invocation: Invocation(display_protocol.ConfigureLayer), _: Answer(void)) isize {
|
||||
const id = targetLayer(invocation.target) orelse return refused;
|
||||
const request = invocation.request;
|
||||
return if (configureLayer(id, request.x, request.y, request.z, request.visible != 0)) 0 else refused;
|
||||
}
|
||||
|
||||
fn onDestroyLayer(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
||||
const id = targetLayer(invocation.target) orelse return refused;
|
||||
return if (destroyLayer(id)) 0 else refused;
|
||||
}
|
||||
|
||||
fn onFillRect(_: void, invocation: Invocation(display_protocol.FillRect), _: Answer(void)) isize {
|
||||
const id = targetLayer(invocation.target) orelse return refused;
|
||||
const request = invocation.request;
|
||||
const local = Rect.init(request.x, request.y, @intCast(request.width), @intCast(request.height));
|
||||
return if (fillLayer(id, local, request.colour)) 0 else refused;
|
||||
}
|
||||
|
||||
fn onBlitTile(_: void, invocation: Invocation(display_protocol.BlitTile), _: Answer(void)) isize {
|
||||
const id = targetLayer(invocation.target) orelse return refused;
|
||||
const request = invocation.request;
|
||||
return if (blitLayer(id, request.x, request.y, request.width, request.height, invocation.tail)) 0 else refused;
|
||||
}
|
||||
|
||||
fn onDamage(_: void, invocation: Invocation(display_protocol.Damage), _: Answer(void)) isize {
|
||||
const id = targetLayer(invocation.target) orelse return refused;
|
||||
const l = layerAt(id) orelse return refused;
|
||||
const request = invocation.request;
|
||||
const screen = Rect{ .x = l.x + request.x, .y = l.y + request.y, .w = @intCast(request.width), .h = @intCast(request.height) };
|
||||
addDamage(screen.intersect(layerScreenRect(l)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn onPresent(_: void, _: Invocation(void), _: Answer(void)) isize {
|
||||
// Scheduled, not immediate: the frame clock composites the accumulated damage
|
||||
// at the next tick, so back-to-back client presents coalesce into one frame.
|
||||
schedulePresent();
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn onSetMode(_: void, invocation: Invocation(display_protocol.SetMode), _: Answer(void)) isize {
|
||||
if (!backend.setMode(invocation.request.width, invocation.request.height)) return refused;
|
||||
addDamage(screenRect()); // repaint the whole screen at the new resolution
|
||||
present();
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn onGetModes(_: void, _: Invocation(void), answer: Answer(display_protocol.Modes)) isize {
|
||||
var list: [4]backend_mod.Mode = undefined;
|
||||
const count = backend.modes(&list);
|
||||
var modes = display_protocol.Modes{ .count = @intCast(count) };
|
||||
for (0..@min(count, display_protocol.max_modes)) |i| {
|
||||
modes.modes[i] = .{ .width = list[i].width, .height = list[i].height };
|
||||
}
|
||||
answer.set(modes);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const handlers = Serve.Handlers{
|
||||
.info = onInfo,
|
||||
.create_layer = onCreateLayer,
|
||||
.configure_layer = onConfigureLayer,
|
||||
.destroy_layer = onDestroyLayer,
|
||||
.fill_rect = onFillRect,
|
||||
.blit_tile = onBlitTile,
|
||||
.damage = onDamage,
|
||||
.present = onPresent,
|
||||
.attach_scanout = onAttachScanout,
|
||||
.set_mode = onSetMode,
|
||||
.get_modes = onGetModes,
|
||||
};
|
||||
|
||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||
_ = sender;
|
||||
if (message.len < display_protocol.request_size) return fail(reply);
|
||||
const request = std.mem.bytesToValue(display_protocol.Request, message[0..display_protocol.request_size]);
|
||||
const payload = message[display_protocol.request_size..];
|
||||
// Switch on the raw operation value — an out-of-range one must fail cleanly, not
|
||||
// panic an `@enumFromInt`.
|
||||
switch (request.operation) {
|
||||
@intFromEnum(display_protocol.Operation.info) => {
|
||||
const m = backend.info();
|
||||
return writeReply(reply, .{ .status = 0, .width = m.width, .height = m.height, .pitch = m.pitch, .format = m.format });
|
||||
},
|
||||
@intFromEnum(display_protocol.Operation.create_layer) => {
|
||||
// x/y are signed coordinates carried in the u32 wire fields — reinterpret the
|
||||
// bits (@bitCast), don't range-check (@intCast) which a negative would fail.
|
||||
const slot = createLayer(@bitCast(request.x), @bitCast(request.y), request.width, request.height, request.z, request.visible != 0) orelse return fail(reply);
|
||||
return writeReply(reply, .{ .status = 0, .layer = slot });
|
||||
},
|
||||
@intFromEnum(display_protocol.Operation.configure_layer) => {
|
||||
return if (configureLayer(request.layer, @bitCast(request.x), @bitCast(request.y), request.z, request.visible != 0)) ok(reply) else fail(reply);
|
||||
},
|
||||
@intFromEnum(display_protocol.Operation.destroy_layer) => {
|
||||
return if (destroyLayer(request.layer)) ok(reply) else fail(reply);
|
||||
},
|
||||
@intFromEnum(display_protocol.Operation.fill_rect) => {
|
||||
const local = Rect.init(@bitCast(request.x), @bitCast(request.y), @intCast(request.width), @intCast(request.height));
|
||||
return if (fillLayer(request.layer, local, request.colour)) ok(reply) else fail(reply);
|
||||
},
|
||||
@intFromEnum(display_protocol.Operation.blit_tile) => {
|
||||
return if (blitLayer(request.layer, @bitCast(request.x), @bitCast(request.y), request.width, request.height, payload)) ok(reply) else fail(reply);
|
||||
},
|
||||
@intFromEnum(display_protocol.Operation.damage) => {
|
||||
const l = layerAt(request.layer) orelse return fail(reply);
|
||||
const screen = Rect{ .x = l.x + @as(i32, @bitCast(request.x)), .y = l.y + @as(i32, @bitCast(request.y)), .w = @intCast(request.width), .h = @intCast(request.height) };
|
||||
addDamage(screen.intersect(layerScreenRect(l)));
|
||||
return ok(reply);
|
||||
},
|
||||
@intFromEnum(display_protocol.Operation.present) => {
|
||||
// Scheduled, not immediate: the frame clock composites the accumulated damage
|
||||
// at the next tick, so back-to-back client presents coalesce into one frame.
|
||||
schedulePresent();
|
||||
return ok(reply);
|
||||
},
|
||||
@intFromEnum(display_protocol.Operation.attach_scanout) => {
|
||||
return attachScanout(request.x, request.width, request.height, request.colour, request.y, arrived, reply);
|
||||
},
|
||||
@intFromEnum(display_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(display_protocol.Operation.get_modes) => {
|
||||
var list: [4]backend_mod.Mode = undefined;
|
||||
const count = backend.modes(&list);
|
||||
var response = display_protocol.ModesReply{ .status = 0, .count = @intCast(count), .modes = undefined };
|
||||
for (0..display_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 one capability this service is ever handed is the scanout driver's
|
||||
// shared surface, and `attachScanout` deliberately does not claim it (the
|
||||
// mapping holds its own reference) — so the capability is peeked, never
|
||||
// taken, and the turn closes it.
|
||||
return Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
|
||||
}
|
||||
|
||||
/// Two notification sources reach the compositor, and one coalesced badge can carry
|
||||
|
||||
Reference in New Issue
Block a user