reorg: move display + scanout protocols to library/protocol

display-protocol -> library/protocol/display/, scanout-protocol ->
library/protocol/scanout/. Consumers import both directly, killing the cryptic
dp/sp aliases in virtio-gpu (now display_protocol / scanout_protocol) and the
runtime.display_protocol / runtime.scanout_protocol re-exports. runtime.display
(the client) still imports display-protocol by name; scanout has no runtime
client, so its runtime module import is dropped too.

zig build + test green; display-service, virtio-gpu, display-native,
display-reattach pass.
This commit is contained in:
Daniel Samson
2026-07-22 21:02:31 +01:00
parent 9ef22d6e55
commit 44122bd44d
9 changed files with 33 additions and 34 deletions
+2 -2
View File
@@ -19,8 +19,8 @@ const dma = runtime.dma;
const shared_memory = runtime.shared_memory;
const system = runtime.system;
const ipc = runtime.ipc;
const display_protocol = runtime.display_protocol;
const scanout_protocol = runtime.scanout_protocol;
const display_protocol = @import("display-protocol");
const scanout_protocol = @import("scanout-protocol");
var device_id: u64 = 0;
fn initialise(endpoint: ipc.Handle) bool {
@@ -7,8 +7,8 @@ const dma = runtime.dma;
const shared_memory = runtime.shared_memory;
const system = runtime.system;
const ipc = runtime.ipc;
const display_protocol = runtime.display_protocol;
const scanout_protocol = runtime.scanout_protocol;
const display_protocol = @import("display-protocol");
const scanout_protocol = @import("scanout-protocol");
const device_manager_protocol = @import("device-manager-protocol");
var device_id: u64 = 0;
+18 -18
View File
@@ -22,8 +22,8 @@ const dma = runtime.dma;
const shared_memory = runtime.shared_memory;
const system = runtime.system;
const ipc = runtime.ipc;
const dp = runtime.display_protocol;
const sp = runtime.scanout_protocol;
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");
@@ -479,15 +479,15 @@ fn announce() void {
std.log.info("no display service to announce to (scanout-only)", .{});
return;
};
var request = dp.Request{
.operation = @intFromEnum(dp.Operation.attach_scanout),
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
.width = current_width,
.height = current_height,
.colour = display_format_bgrx,
};
var reply: [dp.reply_size]u8 = undefined;
var reply: [display_protocol.reply_size]u8 = undefined;
_ = ipc.callCap(display, std.mem.asBytes(&request), &reply, surface.handle) catch {
std.log.info("announce to display failed", .{});
return;
@@ -495,11 +495,11 @@ fn announce() void {
std.log.info("announced scanout to display", .{});
}
/// A `sp.Reply{status}` written into `reply`.
/// A `scanout_protocol.Reply{status}` written into `reply`.
fn scanoutStatus(reply: []u8, ok: bool) usize {
const response = sp.Reply{ .status = if (ok) 0 else -1 };
@memcpy(reply[0..sp.reply_size], std.mem.asBytes(&response));
return sp.reply_size;
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;
}
/// The `.scanout` service: the compositor drives present / mode queries here. The pixels are
@@ -508,22 +508,22 @@ fn scanoutStatus(reply: []u8, ok: bool) usize {
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize {
_ = sender;
_ = capability;
if (message.len < sp.request_size) return 0;
const request = std.mem.bytesToValue(sp.Request, message[0..sp.request_size]);
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(sp.Operation.present) => return scanoutStatus(reply, presentFull()),
@intFromEnum(sp.Operation.get_modes) => {
var response = sp.ModesReply{ .status = 0, .count = offered_modes.len, .modes = undefined };
for (0..sp.max_modes) |i| {
@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..sp.modes_reply_size], std.mem.asBytes(&response));
return sp.modes_reply_size;
@memcpy(reply[0..scanout_protocol.modes_reply_size], std.mem.asBytes(&response));
return scanout_protocol.modes_reply_size;
},
@intFromEnum(sp.Operation.set_mode) => {
@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);
+1 -1
View File
@@ -12,7 +12,7 @@ const compositor = @import("compositor.zig");
const system = runtime.system;
const device = runtime.device;
const ipc = runtime.ipc;
const scanout_protocol = runtime.scanout_protocol;
const scanout_protocol = @import("scanout-protocol");
const Rect = compositor.Rect;
const Surface = compositor.Surface;
+1 -1
View File
@@ -20,7 +20,7 @@ const runtime = @import("runtime");
const compositor = @import("compositor.zig");
const backend_mod = @import("backend.zig");
const protocol = runtime.display_protocol;
const protocol = @import("display-protocol");
const ipc = runtime.ipc;
const system = runtime.system;
const input = runtime.input;
-115
View File
@@ -1,115 +0,0 @@
//! 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).
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,
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,
format: u32 = 0, // a device-abi DisplayFormat value (0 = rgbx, 1 = bgrx)
// create_layer():
layer: u32 = 0,
reserved2: u32 = 0,
};
/// 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,
};
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;
/// 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
/// `fill_rect` request means the same thing to the client that sends it and the
/// compositor that paints it. Little-endian memory, reserved byte 0: rgbx puts red in
/// the low byte, bgrx puts blue there.
pub fn pack(format: u32, r: u8, g: u8, b: u8) u32 {
const rr: u32 = r;
const gg: u32 = g;
const bb: u32 = b;
return switch (format) {
1 => bb | (gg << 8) | (rr << 16), // bgrx
else => rr | (gg << 8) | (bb << 16), // rgbx
};
}
test "pack encodes native byte order for rgbx and bgrx" {
// rgbx: red in the low byte, blue in byte 2.
try std.testing.expectEqual(@as(u32, 0x0000_00AA), pack(0, 0xAA, 0, 0));
try std.testing.expectEqual(@as(u32, 0x00AA_0000), pack(0, 0, 0, 0xAA));
// bgrx: blue in the low byte, red in byte 2.
try std.testing.expectEqual(@as(u32, 0x0000_00AA), pack(1, 0, 0, 0xAA));
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
}
@@ -1,49 +0,0 @@
//! The scanout wire protocol — what the compositor says to a native scanout driver (e.g.
//! virtio-gpu) over its well-known `.scanout` endpoint to put a composited frame on screen.
//! The driver owns the panel and the shared scanout surface it handed the compositor (via the
//! display service's `attach_scanout`); the compositor composites into that surface, then asks
//! the driver to present a damaged rectangle. Tiny by design — one present request. Separate
//! from the display protocol because the directions differ: clients call the compositor over
//! `.display`; the compositor calls the driver over `.scanout`. See docs/display-v2.md.
const std = @import("std");
pub const Operation = enum(u32) {
/// present(x, y, width, height): put the given rectangle of the shared scanout surface on
/// the panel (on virtio-gpu: transfer-to-host of the region, then a fenced resource flush).
present = 0,
/// get_modes() -> ModesReply: the display modes this scanout can switch to (V5).
get_modes = 1,
/// set_mode(width, height): change the scanout resolution — the shared surface is sized to
/// the largest mode, so this just re-points the scanout rectangle; the surface is unchanged.
set_mode = 2,
};
pub const Request = extern struct {
operation: u32,
x: u32 = 0,
y: u32 = 0,
width: u32 = 0,
height: u32 = 0,
};
pub const Reply = extern struct {
status: i32, // 0 on success, negative on failure
reserved: u32 = 0,
};
/// One offered 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 modes.
pub const ModesReply = extern struct {
status: i32,
count: u32,
modes: [max_modes]Mode,
};
pub const message_maximum: usize = 64;
pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply);
pub const modes_reply_size: usize = @sizeOf(ModesReply);