172 lines
7.2 KiB
Zig
172 lines
7.2 KiB
Zig
//! 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);
|
||
}
|