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