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:
@@ -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