166 lines
7.1 KiB
Zig
166 lines
7.1 KiB
Zig
//! User-space display client: talk to the display service (query the mode, and — from D3
|
||
//! — create layers, draw, and present) without hand-rolling the IPC. The `runtime.block`
|
||
//! shape: a cached `/protocol/display` open with a boot-race retry, then extern-struct request/
|
||
//! reply marshalling. See system/services/display/ and docs/display.md.
|
||
|
||
const std = @import("std");
|
||
const channel = @import("channel");
|
||
const envelope = @import("envelope");
|
||
const ipc = @import("ipc");
|
||
const time = @import("time");
|
||
const display_protocol = @import("display-protocol");
|
||
|
||
const Protocol = display_protocol.Protocol;
|
||
|
||
/// The display's current mode, as `info()` reports it.
|
||
pub const Info = struct {
|
||
width: u32,
|
||
height: u32,
|
||
pitch: u32, // bytes per row (may exceed width*4; see docs/framebuffer.md)
|
||
format: u32, // a device-abi DisplayFormat value (0 = rgbx, 1 = bgrx)
|
||
};
|
||
|
||
/// The service endpoint, looked up once and cached.
|
||
var handle: ?ipc.Handle = null;
|
||
|
||
/// Open `/protocol/display`, retrying while it comes up (a client races the
|
||
/// service's bind at boot). Returns the endpoint, or null if it never appears.
|
||
fn service() ?ipc.Handle {
|
||
if (handle) |h| return h;
|
||
var attempts: usize = 0;
|
||
while (attempts < 100) : (attempts += 1) {
|
||
if (channel.openEndpoint("display")) |h| {
|
||
handle = h;
|
||
return h;
|
||
}
|
||
time.sleepMillis(50);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/// A reply the compositor answered with, kept whole so the caller can decode the
|
||
/// verb's own fixed part out of it.
|
||
const Answered = struct {
|
||
packet: [display_protocol.message_maximum]u8,
|
||
len: usize,
|
||
|
||
fn bytes(self: *const Answered) []const u8 {
|
||
return self.packet[0..self.len];
|
||
}
|
||
};
|
||
|
||
/// Send one request (`target` addresses a layer, or 0 for the compositor itself)
|
||
/// and keep the reply. Null when the transport failed or the compositor refused.
|
||
fn transact(
|
||
comptime operation: Protocol.Operation,
|
||
target: u64,
|
||
request: Protocol.RequestOf(operation),
|
||
tail: []const u8,
|
||
) ?Answered {
|
||
const h = service() orelse return null;
|
||
var packet: [display_protocol.message_maximum]u8 = undefined;
|
||
const framed = Protocol.encodeRequest(operation, target, request, tail, &packet) orelse return null;
|
||
var answered: Answered = .{ .packet = undefined, .len = 0 };
|
||
answered.len = ipc.call(h, framed, &answered.packet) catch return null;
|
||
const status = envelope.statusOf(answered.bytes()) orelse return null;
|
||
if (status.status != 0) return null;
|
||
return answered;
|
||
}
|
||
|
||
/// The display's current mode, or null if the service never came up.
|
||
pub fn info() ?Info {
|
||
const answered = transact(.info, 0, {}, &.{}) orelse return null;
|
||
const reply = Protocol.decodeReply(.info, answered.bytes()) orelse return null;
|
||
return .{ .width = reply.width, .height = reply.height, .pitch = reply.pitch, .format = reply.format };
|
||
}
|
||
|
||
/// Composite the dirty layers and flush the frame to the screen.
|
||
pub fn present() bool {
|
||
return transact(.present, 0, {}, &.{}) != null;
|
||
}
|
||
|
||
/// One selectable display mode.
|
||
pub const Mode = display_protocol.Mode;
|
||
|
||
/// Fill `out` with the resolutions the display can switch to; returns how many were written
|
||
/// (zero on the GOP floor, or if the service never came up).
|
||
pub fn modes(out: []Mode) usize {
|
||
const answered = transact(.get_modes, 0, {}, &.{}) orelse return 0;
|
||
const offered = Protocol.decodeReply(.get_modes, answered.bytes()) orelse return 0;
|
||
const count = @min(@min(offered.count, display_protocol.max_modes), out.len);
|
||
for (0..count) |i| out[i] = offered.modes[i];
|
||
return count;
|
||
}
|
||
|
||
/// Change the display resolution. Only a native backend that supports mode-setting honours it
|
||
/// (on the GOP floor it returns false); on success the display's `info()` reports the new mode.
|
||
pub fn setMode(width: u32, height: u32) bool {
|
||
const changed = transact(.set_mode, 0, .{ .width = width, .height = height }, &.{}) != null;
|
||
if (changed) mode = null; // the cached mode is stale now
|
||
return changed;
|
||
}
|
||
|
||
/// The mode, cached after the first `info()` so `color()` doesn't round-trip per pixel.
|
||
var mode: ?Info = null;
|
||
|
||
fn cachedInfo() ?Info {
|
||
if (mode) |m| return m;
|
||
const i = info() orelse return null;
|
||
mode = i;
|
||
return i;
|
||
}
|
||
|
||
/// The native pixel value for an 8-bit-per-channel colour, in the display's format. A
|
||
/// client packs colours through this so it never has to know the byte order itself.
|
||
pub fn color(r: u8, g: u8, b: u8) u32 {
|
||
const format = if (cachedInfo()) |i| i.format else 0;
|
||
return display_protocol.pack(format, r, g, b);
|
||
}
|
||
|
||
/// A handle to a server-owned layer: a positioned, z-ordered surface the client draws
|
||
/// into by command. Create with `createLayer`; drawing and moves take effect on the next
|
||
/// `present`. Coordinates are signed (a layer may sit partly off-screen).
|
||
///
|
||
/// The id is the packet header's `target` on every call below, so it is named once
|
||
/// per request rather than repeated inside one.
|
||
pub const Layer = struct {
|
||
id: u32,
|
||
|
||
/// Fill a rectangle of this layer (layer-local coordinates) with a native `colour`.
|
||
pub fn fill(self: Layer, x: i32, y: i32, w: u32, h: u32, colour: u32) bool {
|
||
return transact(.fill_rect, self.id, .{ .x = x, .y = y, .width = w, .height = h, .colour = colour }, &.{}) != null;
|
||
}
|
||
|
||
/// Copy a `w`×`h` tile of native pixels (row-major, little-endian bytes) into this
|
||
/// layer at (`x`, `y`). The tile rides inline as the request's tail, so `w*h*4` must
|
||
/// fit `display_protocol.maximum_payload` — the bound the protocol derives from this
|
||
/// verb's own fixed part, so the check here can never drift from what fits.
|
||
pub fn blitTile(self: Layer, x: i32, y: i32, w: u32, h: u32, pixels: []const u8) bool {
|
||
if (pixels.len > display_protocol.maximum_payload) return false;
|
||
return transact(.blit_tile, self.id, .{ .x = x, .y = y, .width = w, .height = h }, pixels) != null;
|
||
}
|
||
|
||
/// Move / restack / show or hide the layer.
|
||
pub fn configure(self: Layer, x: i32, y: i32, z: u32, visible: bool) bool {
|
||
return transact(.configure_layer, self.id, .{ .x = x, .y = y, .z = z, .visible = if (visible) 1 else 0 }, &.{}) != null;
|
||
}
|
||
|
||
/// Mark a rectangle of this layer (layer-local) dirty for the next present — for when
|
||
/// the layer's pixels changed without a drawing call the compositor already tracked.
|
||
pub fn damage(self: Layer, x: i32, y: i32, w: u32, h: u32) bool {
|
||
return transact(.damage, self.id, .{ .x = x, .y = y, .width = w, .height = h }, &.{}) != null;
|
||
}
|
||
|
||
/// Release the layer and its surface.
|
||
pub fn destroy(self: Layer) bool {
|
||
return transact(.destroy_layer, self.id, {}, &.{}) != null;
|
||
}
|
||
};
|
||
|
||
/// Create a server-owned layer of `w`×`h` pixels at screen (`x`, `y`) with stacking order
|
||
/// `z` (higher is nearer the front), initially visible. Returns a handle, or null.
|
||
pub fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32) ?Layer {
|
||
const answered = transact(.create_layer, 0, .{ .x = x, .y = y, .width = w, .height = h, .z = z, .visible = 1 }, &.{}) orelse return null;
|
||
return .{ .id = (Protocol.decodeReply(.create_layer, answered.bytes()) orelse return null).layer };
|
||
}
|