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:
@@ -1,93 +1,137 @@
|
||||
//! The display wire protocol — what a client says to the display service over its
|
||||
//! well-known `.display` endpoint. extern-struct messages with an `Operation` tag, the
|
||||
//! same shape as block/vfs/input protocols. The compositor owns the framebuffer and an
|
||||
//! ordered stack of **layers**; a client creates layers, draws into them with these
|
||||
//! operations, marks damage, and asks for a `present`. v1 surfaces are server-owned (a
|
||||
//! client draws by command); shared-memory surfaces are a later milestone (docs/display.md).
|
||||
//! The display wire protocol — what a client says to the display service over
|
||||
//! `/protocol/display`. The compositor owns the framebuffer and an ordered stack of
|
||||
//! **layers**; a client creates layers, draws into them with these operations, marks damage,
|
||||
//! and asks for a `present`. v1 surfaces are server-owned (a client draws by command);
|
||||
//! shared-memory surfaces are a later milestone (docs/display.md).
|
||||
//!
|
||||
//! **`Header.target` is the layer** on every verb that names one — the field that used to be
|
||||
//! `Request.layer`. `info`, `present`, `set_mode`, `get_modes` and `attach_scanout` address
|
||||
//! the compositor itself, so they leave it 0.
|
||||
//!
|
||||
//! Every verb carries its own request type. The single overloaded 40-byte request this
|
||||
//! protocol used to have is gone, and with it the field abuse it invited: `attach_scanout`
|
||||
//! spent `x` on a stride, `y` on a refresh rate and `colour` on a pixel format, which no
|
||||
//! reader could have guessed and no compiler could have caught.
|
||||
|
||||
const envelope = @import("envelope");
|
||||
const std = @import("std");
|
||||
|
||||
pub const Operation = enum(u32) {
|
||||
/// info() -> { width, height, pitch, format }: the display's current mode.
|
||||
info = 0,
|
||||
/// create_layer(x, y, width, height, z) -> { layer }: a new server-owned surface.
|
||||
create_layer = 1,
|
||||
/// configure_layer(layer, x, y, z, visible): move, restack, show, or hide a layer.
|
||||
configure_layer = 2,
|
||||
/// destroy_layer(layer): release a layer.
|
||||
destroy_layer = 3,
|
||||
/// fill_rect(layer, x, y, width, height, colour): fill a rectangle of a layer.
|
||||
fill_rect = 4,
|
||||
/// blit_tile(layer, x, y, width, height, <inline pixels>): copy a small pixel tile in.
|
||||
blit_tile = 5,
|
||||
/// damage(layer, x, y, width, height): mark a region dirty for the next present.
|
||||
damage = 6,
|
||||
/// present(): composite the dirty layers and flush to the screen.
|
||||
present = 7,
|
||||
/// attach_scanout(x=stride, y=refresh_hz, width, height, colour=format) + <surface
|
||||
/// capability>: a native scanout driver announces itself, handing over the shared scanout
|
||||
/// surface as an `ipc_call` send_cap. The compositor maps it, looks up the driver's
|
||||
/// `.scanout` present channel, and upgrades off the GOP floor (docs/display-v2.md V4).
|
||||
/// `x` is the surface's row stride in pixels, `y` the panel refresh rate from the
|
||||
/// driver's EDID read (0 = unknown; paces the compositor's frame clock), `colour` the
|
||||
/// DisplayFormat.
|
||||
attach_scanout = 8,
|
||||
/// set_mode(width, height): change the display resolution — only a native backend that
|
||||
/// reports `canModeSet` honours it; on the GOP floor it fails (docs/display-v2.md V5).
|
||||
set_mode = 9,
|
||||
/// get_modes() -> ModesReply: the resolutions the display can switch to (empty on GOP).
|
||||
get_modes = 10,
|
||||
};
|
||||
|
||||
/// The fixed request header. A `blit_tile`'s pixel payload (width*height 32-bit pixels)
|
||||
/// follows this header inline in the same message, up to `maximum_payload`.
|
||||
pub const Request = extern struct {
|
||||
operation: u32,
|
||||
layer: u32 = 0, // create/configure/destroy/fill/blit/damage: the target layer
|
||||
x: u32 = 0,
|
||||
y: u32 = 0,
|
||||
/// The answer to `info()`: the display's current mode.
|
||||
pub const Info = extern struct {
|
||||
width: u32 = 0,
|
||||
height: u32 = 0,
|
||||
z: u32 = 0, // create_layer / configure_layer: stacking order (higher = in front)
|
||||
colour: u32 = 0, // fill_rect: the fill colour (native pixel value)
|
||||
visible: u32 = 1, // configure_layer: 0 hides the layer
|
||||
reserved: u32 = 0,
|
||||
};
|
||||
|
||||
pub const Reply = extern struct {
|
||||
status: i32, // 0 on success, negative on failure
|
||||
reserved: u32 = 0,
|
||||
// info():
|
||||
width: u32 = 0,
|
||||
height: u32 = 0,
|
||||
pitch: u32 = 0,
|
||||
pitch: u32 = 0, // bytes per row (may exceed width*4)
|
||||
format: u32 = 0, // a device-abi DisplayFormat value (0 = rgbx, 1 = bgrx)
|
||||
// create_layer():
|
||||
layer: u32 = 0,
|
||||
reserved2: u32 = 0,
|
||||
};
|
||||
|
||||
/// `create_layer(...)`: a new server-owned surface. Coordinates are signed — a layer may sit
|
||||
/// partly off-screen.
|
||||
pub const CreateLayer = extern struct {
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
z: u32 = 0, // stacking order (higher = nearer the front)
|
||||
visible: u32 = 1,
|
||||
};
|
||||
|
||||
/// The layer a `create_layer` established — the integer later packets put in `Header.target`.
|
||||
pub const Created = extern struct { layer: u32 };
|
||||
|
||||
/// `configure_layer(...)` on `Header.target`: move, restack, show, or hide it.
|
||||
pub const ConfigureLayer = extern struct {
|
||||
x: i32,
|
||||
y: i32,
|
||||
z: u32 = 0,
|
||||
visible: u32 = 1, // 0 hides the layer
|
||||
};
|
||||
|
||||
/// `fill_rect(...)` on `Header.target`: fill a layer-local rectangle with a native pixel value.
|
||||
pub const FillRect = extern struct {
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
colour: u32,
|
||||
};
|
||||
|
||||
/// `blit_tile(...)` on `Header.target`: copy a `width`×`height` tile of native pixels
|
||||
/// (row-major, little-endian) into the layer. The pixels ride inline as the packet's tail,
|
||||
/// up to `maximum_payload`.
|
||||
pub const BlitTile = extern struct {
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
};
|
||||
|
||||
/// `damage(...)` on `Header.target`: mark a layer-local region dirty for the next present.
|
||||
pub const Damage = extern struct {
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
};
|
||||
|
||||
/// `attach_scanout(...)` + the shared surface as the call's capability: a native scanout
|
||||
/// driver announces itself. The compositor maps the surface, opens the driver's
|
||||
/// `/protocol/scanout` present channel, and upgrades off the GOP floor (docs/display-v2.md
|
||||
/// V4). Each field says what it is, which the old shared request could not.
|
||||
pub const AttachScanout = extern struct {
|
||||
/// The surface's row stride in pixels (it is sized to the driver's largest mode).
|
||||
stride: u32,
|
||||
/// The active mode within that surface.
|
||||
width: u32,
|
||||
height: u32,
|
||||
/// A device-abi DisplayFormat value.
|
||||
format: u32,
|
||||
/// The panel refresh rate from the driver's EDID read (0 = unknown); it paces the
|
||||
/// compositor's frame clock.
|
||||
refresh_hz: u32 = 0,
|
||||
};
|
||||
|
||||
/// `set_mode(width, height)`: change the display resolution — only a native backend that
|
||||
/// reports `canModeSet` honours it; on the GOP floor it fails (docs/display-v2.md V5).
|
||||
pub const SetMode = extern struct { width: u32, height: u32 };
|
||||
|
||||
/// One selectable display mode.
|
||||
pub const Mode = extern struct { width: u32, height: u32 };
|
||||
pub const max_modes = 4;
|
||||
|
||||
/// The reply to `get_modes`: a small fixed list of resolutions the display can switch to.
|
||||
pub const ModesReply = extern struct {
|
||||
status: i32,
|
||||
count: u32,
|
||||
modes: [max_modes]Mode,
|
||||
/// The answer to `get_modes`: the resolutions the display can switch to (empty on GOP).
|
||||
pub const Modes = extern struct {
|
||||
count: u32 = 0,
|
||||
_padding: u32 = 0,
|
||||
modes: [max_modes]Mode = @splat(.{ .width = 0, .height = 0 }),
|
||||
};
|
||||
pub const modes_reply_size: usize = @sizeOf(ModesReply);
|
||||
|
||||
/// The IPC message size — the kernel caps every message at `MESSAGE_MAXIMUM` (256 bytes,
|
||||
/// system/kernel/ipc-synchronous.zig), so this matches it (a larger receive/reply buffer
|
||||
/// is rejected with -E2BIG). A `blit_tile` therefore carries only a *small* tile inline —
|
||||
/// `maximum_payload` bytes = up to 54 pixels, enough for a cursor or small sprite; larger
|
||||
/// bitmaps are the deferred shared-memory surface path (docs/display.md).
|
||||
pub const message_maximum: usize = 256;
|
||||
pub const request_size: usize = @sizeOf(Request);
|
||||
pub const reply_size: usize = @sizeOf(Reply);
|
||||
pub const maximum_payload: usize = message_maximum - request_size;
|
||||
pub const Protocol = envelope.Define(.{
|
||||
.name = "display",
|
||||
.version = 1,
|
||||
.operations = &.{
|
||||
.{ .name = "info", .reply = Info },
|
||||
.{ .name = "create_layer", .request = CreateLayer, .reply = Created },
|
||||
.{ .name = "configure_layer", .request = ConfigureLayer },
|
||||
.{ .name = "destroy_layer" },
|
||||
.{ .name = "fill_rect", .request = FillRect },
|
||||
.{ .name = "blit_tile", .request = BlitTile },
|
||||
.{ .name = "damage", .request = Damage },
|
||||
.{ .name = "present" },
|
||||
.{ .name = "attach_scanout", .request = AttachScanout },
|
||||
.{ .name = "set_mode", .request = SetMode },
|
||||
.{ .name = "get_modes", .reply = Modes },
|
||||
},
|
||||
});
|
||||
|
||||
pub const Operation = Protocol.Operation;
|
||||
pub const message_maximum: usize = Protocol.message_maximum;
|
||||
|
||||
/// The largest inline pixel tile a `blit_tile` may carry: the call floor less the header and
|
||||
/// this verb's own fixed part — 224 bytes, up to 56 pixels, enough for a cursor or a small
|
||||
/// sprite. Larger bitmaps are the deferred shared-memory surface path (docs/display.md).
|
||||
/// Per-verb rather than protocol-wide, because with per-operation requests there is no
|
||||
/// single "request size" to subtract any more.
|
||||
pub const maximum_payload: usize = envelope.packet_maximum - envelope.prefix_size - @sizeOf(BlitTile);
|
||||
|
||||
/// Pack an 8-bit-per-channel colour into the display's native 32-bit pixel for `format`
|
||||
/// (a device-abi `DisplayFormat`: 0 = rgbx, 1 = bgrx). Shared so a `colour` in a
|
||||
@@ -113,3 +157,15 @@ test "pack encodes native byte order for rgbx and bgrx" {
|
||||
try std.testing.expectEqual(@as(u32, 0x00AA_0000), pack(1, 0xAA, 0, 0));
|
||||
try std.testing.expectEqual(@as(u32, 0x0000_3020), pack(0, 0x20, 0x30, 0)); // green in byte 1
|
||||
}
|
||||
|
||||
test "the layer rides the header, and the blit tile grew with the split" {
|
||||
var buffer: [message_maximum]u8 = undefined;
|
||||
const pixels = [_]u8{0xFF} ** 16;
|
||||
const packet = Protocol.encodeRequest(.blit_tile, 3, .{ .x = 1, .y = 2, .width = 2, .height = 2 }, &pixels, &buffer).?;
|
||||
try std.testing.expectEqual(@as(u64, 3), envelope.headerOf(packet).?.target);
|
||||
try std.testing.expectEqual(@as(i32, 1), Protocol.decodeRequest(.blit_tile, packet).?.x);
|
||||
try std.testing.expectEqual(@as(usize, 16), Protocol.requestTail(.blit_tile, packet).len);
|
||||
// 216 bytes under the old 40-byte shared request; the header plus this
|
||||
// verb's own four fields is 32.
|
||||
try std.testing.expectEqual(@as(usize, 224), maximum_payload);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user