reorg: begin library/protocol/ — move block + usb-transfer

Start the protocol tree: wire protocols move to library/protocol/<name>/,
entry file <name>-protocol.zig, module name unchanged. block and usb-transfer
are pure moves — every consumer already imports them by module name, so only
the b.addModule paths change and the empty system/services/block/ is removed.

  block-protocol         -> library/protocol/block/block-protocol.zig
  usb-transfer-protocol  -> library/protocol/usb-transfer/usb-transfer-protocol.zig

zig build + test green.
This commit is contained in:
Daniel Samson
2026-07-22 20:49:07 +01:00
parent 1d1234c963
commit f90dc6c121
4 changed files with 10 additions and 2 deletions
@@ -1,159 +0,0 @@
//! The USB transfer protocol: what a USB class driver (a keyboard, mouse, or
//! mass-storage driver) says to the xHCI bus driver over its well-known
//! `.usb_bus` endpoint to drive its device. The class driver owns no hardware —
//! it reaches its device entirely through these messages, the way a PS/2 keyboard
//! driver reaches the 8042 through the ps2-bus. Extern-struct messages tagged by
//! `Operation`, the vfs-protocol / device-manager-protocol pattern.
//!
//! The shape:
//! - **open** (a capability-passing `ipc.callCap`): the class driver hands over
//! its own endpoint (for asynchronous interrupt reports) and its assigned
//! device id, and receives a `device_token` plus its interface's endpoints.
//! - **control / bulk** (synchronous `ipc.call`): one transfer, answered when
//! it completes. Control data travels inline (descriptors, HID/MSC class
//! requests are all small); bulk data travels by **physical address** — the
//! class driver's own `dma_alloc`'d buffer — so a 512-byte sector never has
//! to cross the 256-byte IPC boundary.
//! - **interrupt_subscribe** (synchronous): arm periodic IN polling of an
//! interrupt endpoint; each report the device produces is then pushed to the
//! class driver's endpoint as an asynchronous `InterruptReport` (`ipc.send`),
//! exactly how the input service delivers events.
//!
//! Single controller assumption: one `.usb_bus` singleton serves QEMU's one xHCI.
//! A multi-controller machine would need a per-controller endpoint (the device
//! manager handing each class driver the right one); noted, not built.
/// Fits one synchronous IPC message (kernel MESSAGE_MAXIMUM).
pub const message_maximum: usize = 256;
/// The largest inline control-transfer payload. Sized so a whole message
/// (header + data) stays under `message_maximum`: descriptors and HID/MSC class
/// requests are all far smaller.
pub const max_inline_data: usize = 200;
/// The largest interrupt report pushed asynchronously. Sized so `InterruptReport`
/// fits an `ipc_send` payload slot (POST_MAXIMUM = 64): boot keyboard reports are
/// 8 bytes, boot mouse reports 3–4.
pub const max_report_data: usize = 48;
/// Endpoints per interface reported back in an open reply (a boot HID interface
/// has one interrupt endpoint, a mass-storage interface two bulk endpoints).
pub const max_reported_endpoints: usize = 4;
pub const Operation = enum(u32) {
open = 0,
control = 1,
interrupt_subscribe = 2,
bulk = 3,
};
/// The endpoint facts a class driver needs, lifted from the endpoint descriptor
/// the bus driver already parsed during enumeration.
pub const Endpoint = extern struct {
/// EndpointDescriptor address: direction in bit 7, number in bits 3:0.
address: u8,
/// 0 control, 1 isochronous, 2 bulk, 3 interrupt.
transfer_type: u8,
max_packet_size: u16,
interval: u8,
reserved: [3]u8 = .{ 0, 0, 0 },
};
/// open: the class driver's receive endpoint rides as the call's capability, and
/// `device_id` is the interface's assigned id (its argv[1]).
pub const OpenRequest = extern struct {
operation: u32 = @intFromEnum(Operation.open),
reserved: u32 = 0,
device_id: u64,
};
/// The answer to open: a token scoping every later request to this device, the
/// interface's class triple (a sanity check), and its endpoints.
pub const OpenReply = extern struct {
status: i32,
endpoint_count: u32,
device_token: u64,
interface_class: u8,
interface_subclass: u8,
interface_protocol: u8,
interface_number: u8,
reserved2: u32 = 0,
endpoints: [max_reported_endpoints]Endpoint = [_]Endpoint{.{ .address = 0, .transfer_type = 0, .max_packet_size = 0, .interval = 0 }} ** max_reported_endpoints,
};
/// control: one EP0 control transfer. `setup` is a bit-cast `usb_abi.Request`.
/// For an OUT transfer `data[0..data_length]` is sent; for an IN transfer the
/// reply carries up to `data_length` bytes back.
pub const ControlRequest = extern struct {
operation: u32 = @intFromEnum(Operation.control),
reserved: u32 = 0,
device_token: u64,
setup: [8]u8,
direction_in: u8, // 1 = device-to-host (IN), 0 = host-to-device (OUT)
reserved2: u8 = 0,
data_length: u16,
reserved3: u32 = 0,
data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
};
pub const ControlReply = extern struct {
status: i32, // 0 success, negative on failure/stall
actual_length: u32,
data: [max_inline_data]u8 = [_]u8{0} ** max_inline_data,
};
/// interrupt_subscribe: begin periodic IN polling of an interrupt endpoint. Each
/// report the device returns is pushed to the caller's endpoint (handed over at
/// open) as an asynchronous `InterruptReport`.
pub const InterruptSubscribeRequest = extern struct {
operation: u32 = @intFromEnum(Operation.interrupt_subscribe),
reserved: u32 = 0,
device_token: u64,
endpoint_address: u8,
reserved2: u8 = 0,
max_length: u16, // bytes to request per poll (the endpoint's max packet size)
};
pub const InterruptSubscribeReply = extern struct {
status: i32,
reserved: u32 = 0,
};
/// bulk: one bulk IN or OUT transfer. `physical_address` is the class driver's own
/// `dma_alloc`'d buffer — the controller DMAs straight to/from it, so the bulk
/// data never crosses IPC. `endpoint_address`'s bit 7 selects IN vs OUT.
pub const BulkRequest = extern struct {
operation: u32 = @intFromEnum(Operation.bulk),
reserved: u32 = 0,
device_token: u64,
physical_address: u64,
length: u32,
endpoint_address: u8,
reserved2: u8 = 0,
reserved3: u16 = 0,
};
pub const BulkReply = extern struct {
status: i32,
actual_length: u32,
};
/// An asynchronous interrupt report, pushed with `ipc.send` to a subscriber's
/// endpoint. `Received.isMessage()` is set; there is no reply owed.
pub const InterruptReport = extern struct {
device_token: u64,
endpoint_address: u8,
length: u8,
reserved: u16 = 0,
data: [max_report_data]u8 = [_]u8{0} ** max_report_data,
};
comptime {
const std = @import("std");
// Every synchronous message must fit one IPC message; the async report must
// fit an ipc_send payload slot.
std.debug.assert(@sizeOf(ControlRequest) <= message_maximum);
std.debug.assert(@sizeOf(ControlReply) <= message_maximum);
std.debug.assert(@sizeOf(OpenReply) <= message_maximum);
std.debug.assert(@sizeOf(InterruptReport) <= 64);
}
-44
View File
@@ -1,44 +0,0 @@
//! The block-device wire protocol — what a filesystem (the FAT server) says to a
//! block driver (usb-storage) over its well-known `.block` endpoint. A protocol
//! module like vfs-protocol / usb-transfer-protocol: extern-struct messages, an
//! `Operation` tag, everything in one IPC message.
//!
//! Data path: read and write move whole blocks to or from a **caller-owned DMA
//! buffer**, named by its physical address — the same physical-address handoff
//! usb-storage already uses toward the controller, one layer up. So a 512-byte
//! sector never has to cross the 256-byte IPC boundary; only the small request /
//! reply headers do. (Safe while the IOMMU is unenforced; see docs/driver-model.md.)
pub const Operation = enum(u32) {
/// geometry() -> { block_size, block_count }
geometry = 0,
/// read(lba, count, physical): read `count` blocks from `lba` into the buffer
read = 1,
/// write(lba, count, physical): write `count` blocks at `lba` from the buffer
write = 2,
/// flush(): commit any device write cache to stable media (no data transfer).
/// A filesystem calls this to make prior writes durable — e.g. before power-off,
/// so a shutdown-time write isn't lost in the USB flash controller's cache.
flush = 3,
};
pub const Request = extern struct {
operation: u32,
reserved: u32 = 0,
lba: u64,
count: u32, // number of blocks (read/write)
reserved2: u32 = 0,
physical: u64, // caller's DMA buffer physical address (read/write)
};
pub const Reply = extern struct {
status: i32, // 0 on success, negative on failure
reserved: u32 = 0,
block_size: u32, // geometry: bytes per block (512)
reserved2: u32 = 0,
block_count: u64, // geometry: total blocks; read/write: blocks moved
};
pub const message_maximum: usize = 256;
pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);
@@ -42,6 +42,13 @@ const virtio_gpu_pci_class: u64 = pci_class.ClassCode.pack(.{
.prog_if = 0,
});
const vga_compatible_gpu_pci_class: u64 = pci_class.ClassCode.pack(.{
.base = @intFromEnum(pci_class.BaseClass.display),
.subclass = @intFromEnum(pci_class.display.SubClass.vga_compatible),
.prog_if = 0,
});
/// The driver that serves a *reported* PCI function (M19.3: matching moved
/// from the boot snapshot to the bus reports), or null. A machine can carry
/// several identical controllers — one driver instance per reported device,
@@ -49,6 +56,7 @@ const virtio_gpu_pci_class: u64 = pci_class.ClassCode.pack(.{
fn pciDriverForIdentity(identity: u64) ?[]const u8 {
return switch (identity) {
xhci_pci_class => "/system/drivers/usb-xhci-bus",
vga_compatible_gpu_pci_class => "/system/drivers/display",
virtio_gpu_pci_class => "/system/drivers/virtio-gpu",
else => null,
};