//! 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 }; }