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:
Daniel Samson
2026-08-01 06:15:25 +01:00
parent b004b9c3eb
commit d2dfbcabf8
40 changed files with 1882 additions and 1008 deletions
+2 -2
View File
@@ -10,8 +10,8 @@ pub fn build(b: *std.Build) void {
.name = "usb-storage",
.root_source_file = b.path("usb-storage.zig"),
.imports = &.{
"block-protocol", "driver", "ipc", "logging", "memory", "process", "service",
"time", "usb",
"block-protocol", "driver", "envelope", "ipc", "logging", "memory", "process",
"service", "time", "usb",
},
});
b.installArtifact(exe);
+65 -45
View File
@@ -22,8 +22,16 @@ const logging = @import("logging");
const usb = @import("usb");
const scsi = @import("scsi.zig");
const bot = @import("bulk-only-transport.zig");
const envelope = @import("envelope");
const block_protocol = @import("block-protocol");
/// The generated block dispatch. One device per process, so the handler context
/// is empty and the geometry stays in this file's globals.
const Serve = block_protocol.Protocol.Provider(void);
const Invocation = envelope.Invocation;
const Answer = envelope.Answer;
var device_id: u64 = 0;
var device: usb.Device = undefined;
var bulk_in: usb.Endpoint = undefined;
@@ -144,53 +152,65 @@ fn initialise(endpoint: ipc.Handle) bool {
return true;
}
/// Serve the block protocol: geometry, and whole-block read/write to/from the
/// caller's DMA buffer (named by physical address).
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
_ = sender;
if (message.len < block_protocol.request_size) return 0;
const request = std.mem.bytesToValue(block_protocol.Request, message[0..block_protocol.request_size]);
switch (request.operation) {
@intFromEnum(block_protocol.Operation.attach) => {
// The filesystem's DMA buffer: forward its capability to the controller
// so the device can reach it. Never claimed — the binding holds its own
// reference, so our copy is the turn's to close, on this path and on the
// refusal above it alike.
const handle = arrived.peek() orelse return writeReply(reply, .{ .status = -1, .block_size = 0, .block_count = 0 });
const ok = device.attachDma(handle);
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = 0, .block_count = 0 });
},
@intFromEnum(block_protocol.Operation.geometry) => {
return writeReply(reply, .{ .status = 0, .block_size = block_size, .block_count = block_count });
},
@intFromEnum(block_protocol.Operation.read) => {
const count: u16 = @intCast(request.count);
const cdb = scsi.read10(@intCast(request.lba), count);
const ok = transact(&cdb, true, request.physical, request.count * block_size);
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = if (ok) request.count else 0 });
},
@intFromEnum(block_protocol.Operation.write) => {
const count: u16 = @intCast(request.count);
const cdb = scsi.write10(@intCast(request.lba), count);
const ok = transact(&cdb, false, request.physical, request.count * block_size);
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = if (ok) request.count else 0 });
},
@intFromEnum(block_protocol.Operation.flush) => {
// SYNCHRONIZE CACHE: commit the device's write cache to flash. No data
// stage. Makes prior writes durable before a caller (init at shutdown)
// cuts power. A device without a volatile cache reports success anyway.
const cdb = scsi.synchronizeCache10();
const ok = transact(&cdb, false, 0, 0);
return writeReply(reply, .{ .status = if (ok) 0 else -1, .block_size = block_size, .block_count = 0 });
},
else => return 0,
}
// --- serving the block protocol ---------------------------------------------
//
// Geometry, and whole-block read/write to and from the caller's DMA buffer
// (named by physical address). One device per process, so `Header.target` is
// always 0 and no handler reads it.
/// A transfer the device refused. Every failure here is the same one — the SCSI
/// command did not complete — so there is one errno for all of them.
const refused: isize = -envelope.ENOENT;
fn onGeometry(_: void, _: Invocation(void), answer: Answer(block_protocol.Geometry)) isize {
answer.set(.{ .block_size = block_size, .block_count = block_count });
return 0;
}
fn writeReply(reply: []u8, value: block_protocol.Reply) usize {
const bytes = std.mem.asBytes(&value);
@memcpy(reply[0..bytes.len], bytes);
return bytes.len;
fn onRead(_: void, invocation: Invocation(block_protocol.Transfer), answer: Answer(block_protocol.Transferred)) isize {
const request = invocation.request;
const cdb = scsi.read10(@intCast(request.lba), @intCast(request.count));
if (!transact(&cdb, true, request.physical, request.count * block_size)) return refused;
answer.set(.{ .count = request.count });
return 0;
}
fn onWrite(_: void, invocation: Invocation(block_protocol.Transfer), answer: Answer(block_protocol.Transferred)) isize {
const request = invocation.request;
const cdb = scsi.write10(@intCast(request.lba), @intCast(request.count));
if (!transact(&cdb, false, request.physical, request.count * block_size)) return refused;
answer.set(.{ .count = request.count });
return 0;
}
/// SYNCHRONIZE CACHE: commit the device's write cache to flash. No data stage.
/// Makes prior writes durable before a caller (init at shutdown) cuts power. A
/// device without a volatile cache reports success anyway.
fn onFlush(_: void, _: Invocation(void), _: Answer(void)) isize {
const cdb = scsi.synchronizeCache10();
return if (transact(&cdb, false, 0, 0)) 0 else refused;
}
/// The filesystem's DMA buffer: forward its capability to the controller so the
/// device can reach it. Never claimed — the binding holds its own reference, so
/// our copy is the turn's to close, on this path and on the refusal alike.
fn onAttach(_: void, invocation: Invocation(void), _: Answer(void)) isize {
const handle = invocation.capability orelse return -envelope.EPROTO;
return if (device.attachDma(handle)) 0 else refused;
}
const handlers = Serve.Handlers{
.geometry = onGeometry,
.read = onRead,
.write = onWrite,
.flush = onFlush,
.attach = onAttach,
};
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
// Peeked, never taken: `attach` forwards the capability and the controller's
// binding takes its own reference, so this copy stays the turn's to close.
return Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
}
pub fn main(init: process.Init) void {
+2 -2
View File
@@ -10,8 +10,8 @@ pub fn build(b: *std.Build) void {
.name = "virtio-gpu",
.root_source_file = b.path("virtio-gpu.zig"),
.imports = &.{
"channel", "display-protocol", "driver", "ipc", "logging", "memory", "mmio", "pci",
"process", "scanout-protocol", "service", "time",
"channel", "display-protocol", "driver", "envelope", "ipc", "logging", "memory",
"mmio", "pci", "process", "scanout-protocol", "service", "time",
},
});
b.installArtifact(exe);
+52 -44
View File
@@ -25,11 +25,19 @@ const memory = @import("memory");
const logging = @import("logging");
const mmio = @import("mmio");
const pci = @import("pci");
const envelope = @import("envelope");
const display_protocol = @import("display-protocol");
const scanout_protocol = @import("scanout-protocol");
const vp = @import("virtio-pci.zig");
const vg = @import("virtio-gpu-protocol.zig");
/// The generated scanout dispatch. One scanout per driver instance, so the
/// handler context is empty and the mode stays in this file's globals.
const Serve = scanout_protocol.Protocol.Provider(void);
const Invocation = envelope.Invocation;
const Answer = envelope.Answer;
/// The DisplayFormat (device-abi) our B8G8R8X8 scanout resource presents: bgrx = 1. Handed to
/// the compositor in the announce so it packs colours in the surface's byte order.
const display_format_bgrx: u32 = 1;
@@ -482,60 +490,60 @@ fn announce() void {
std.log.info("no display service to announce to (scanout-only)", .{});
return;
};
var request = display_protocol.Request{
.operation = @intFromEnum(display_protocol.Operation.attach_scanout),
.x = max_width, // the shared surface's row stride in pixels (it is sized to the max mode)
.y = edid_refresh_hz, // the panel refresh from EDID (0 = unknown) — the frame-clock seed
var packet: [display_protocol.message_maximum]u8 = undefined;
const framed = display_protocol.Protocol.encodeRequest(.attach_scanout, 0, .{
.stride = 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: [display_protocol.reply_size]u8 = undefined;
_ = ipc.callCap(display, std.mem.asBytes(&request), &reply, surface.handle) catch {
.format = display_format_bgrx,
.refresh_hz = edid_refresh_hz, // from EDID (0 = unknown) — the frame-clock seed
}, &.{}, &packet) orelse return;
var reply: [display_protocol.message_maximum]u8 = undefined;
_ = ipc.callCap(display, framed, &reply, surface.handle) catch {
std.log.info("announce to display failed", .{});
return;
};
std.log.info("announced scanout to display", .{});
}
/// A `scanout_protocol.Reply{status}` written into `reply`.
fn scanoutStatus(reply: []u8, ok: bool) usize {
const response = scanout_protocol.Reply{ .status = if (ok) 0 else -1 };
@memcpy(reply[0..scanout_protocol.reply_size], std.mem.asBytes(&response));
return scanout_protocol.reply_size;
// --- serving the scanout protocol -------------------------------------------
//
// 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). One scanout, so `Header.target` is always 0.
/// The device did not take the frame, or the mode asked for is not one this
/// scanout offers.
const refused: isize = -envelope.ENOENT;
fn onPresent(_: void, _: Invocation(scanout_protocol.Present), _: Answer(void)) isize {
return if (presentFull()) 0 else refused;
}
/// 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, arrived: *ipc.Arrival) usize {
_ = sender;
_ = arrived; // nothing here takes a capability: the harness closes what arrives
if (message.len < scanout_protocol.request_size) return 0;
const request = std.mem.bytesToValue(scanout_protocol.Request, message[0..scanout_protocol.request_size]);
switch (request.operation) {
@intFromEnum(scanout_protocol.Operation.present) => return scanoutStatus(reply, presentFull()),
@intFromEnum(scanout_protocol.Operation.get_modes) => {
var response = scanout_protocol.ModesReply{ .status = 0, .count = offered_modes.len, .modes = undefined };
for (0..scanout_protocol.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..scanout_protocol.modes_reply_size], std.mem.asBytes(&response));
return scanout_protocol.modes_reply_size;
},
@intFromEnum(scanout_protocol.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,
fn onGetModes(_: void, _: Invocation(void), answer: Answer(scanout_protocol.Modes)) isize {
var offered = scanout_protocol.Modes{ .count = offered_modes.len };
for (0..@min(offered_modes.len, scanout_protocol.max_modes)) |i| {
offered.modes[i] = .{ .width = offered_modes[i].width, .height = offered_modes[i].height };
}
answer.set(offered);
return 0;
}
fn onSetMode(_: void, invocation: Invocation(scanout_protocol.SetMode), _: Answer(void)) isize {
const w = invocation.request.width;
const h = invocation.request.height;
if (w == 0 or h == 0 or w > max_width or h > max_height) return refused;
current_width = w;
current_height = h;
return if (setScanoutRect()) 0 else refused;
}
const handlers = Serve.Handlers{ .present = onPresent, .get_modes = onGetModes, .set_mode = onSetMode };
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
_ = arrived; // nothing here takes a capability: the harness closes what arrives
return Serve.dispatch({}, handlers, message, sender, null, reply);
}
pub fn main(init: process.Init) void {
@@ -547,7 +555,7 @@ pub fn main(init: process.Init) void {
std.log.info("malformed device id '{s}'", .{argument});
return;
};
service.run(256, .{
service.run(scanout_protocol.message_maximum, .{
.service = "scanout",
.init = initialise,
.on_message = onMessage,