danos/library/protocol/display/display-protocol.zig

172 lines
7.2 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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");
/// The answer to `info()`: the display's current mode.
pub const Info = extern struct {
width: u32 = 0,
height: 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(...)`: 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 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 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
/// `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
}
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);
}