398 lines
15 KiB
Zig
398 lines
15 KiB
Zig
//! /system/services/display — the display service (docs/display.md). A ring-3 process
|
|
//! that claims the framebuffer the kernel seeded (docs/display-plan.md D1), owns it as a
|
|
//! **write-combining front buffer**, composites an ordered stack of **layers** into a
|
|
//! **cacheable back buffer**, and presents finished frames — the GUI track's compositor,
|
|
//! the sibling of the input service. Reached by name over `ServiceId.display`.
|
|
//!
|
|
//! A layer is a server-owned surface (its own cacheable buffer) with a screen position,
|
|
//! z-order, and visibility. Clients create layers and draw into them by command
|
|
//! (`fill_rect`, `blit_tile`), mark `damage`, and ask for a `present`; the compositor
|
|
//! repaints only the damaged region — clear it, paint the visible layers bottom-to-top,
|
|
//! flush it to the screen. The pixel math lives in the pure, host-tested
|
|
//! [compositor.zig](compositor.zig); this file wires real surfaces and the framebuffer to
|
|
//! it. Shared-memory client surfaces are a later milestone (docs/display.md).
|
|
|
|
const std = @import("std");
|
|
const runtime = @import("runtime");
|
|
const compositor = @import("compositor.zig");
|
|
|
|
const protocol = runtime.display_protocol;
|
|
const ipc = runtime.ipc;
|
|
const system = runtime.system;
|
|
const device = runtime.device;
|
|
const Rect = compositor.Rect;
|
|
const Surface = compositor.Surface;
|
|
|
|
/// The claimed framebuffer and its off-screen twin. The front buffer is the LFB —
|
|
/// write-combining, so it is **only ever written**, never read; all compositing happens
|
|
/// in the cacheable back buffer, which is then streamed to the front (docs/display.md).
|
|
const Display = struct {
|
|
device_id: u64,
|
|
front: [*]volatile u8, // the LFB (write-combining)
|
|
back: [*]u8, // cacheable, same geometry
|
|
width: u32,
|
|
height: u32,
|
|
pitch: u32, // bytes per row (shared by both buffers)
|
|
format: u32, // a device-abi DisplayFormat value
|
|
frames: u64 = 0,
|
|
};
|
|
|
|
var display: Display = undefined;
|
|
|
|
/// The wallpaper the compositor clears damaged regions to before painting layers.
|
|
var background: u32 = 0;
|
|
|
|
/// The layer stack. A fixed table (a compositor has few top-level surfaces during
|
|
/// bring-up); each used slot owns an mmap'd surface. `damage` accumulates the dirty
|
|
/// screen region since the last `present`, so a present touches only what changed.
|
|
const maximum_layers = 16;
|
|
|
|
const Layer = struct {
|
|
used: bool = false,
|
|
x: i32 = 0,
|
|
y: i32 = 0,
|
|
z: u32 = 0,
|
|
visible: bool = false,
|
|
surface: Surface = undefined,
|
|
surface_len: usize = 0, // for munmap on destroy
|
|
};
|
|
|
|
var layers: [maximum_layers]Layer = [_]Layer{.{}} ** maximum_layers;
|
|
var damage: Rect = Rect.empty;
|
|
|
|
/// Enumeration buffer kept off the stack — a `DeviceDescriptor` is large, and this
|
|
/// service only ever needs one scan.
|
|
var device_table: [64]device.DeviceDescriptor = undefined;
|
|
|
|
// --- geometry helpers -------------------------------------------------------
|
|
|
|
fn screenRect() Rect {
|
|
return .{ .x = 0, .y = 0, .w = @intCast(display.width), .h = @intCast(display.height) };
|
|
}
|
|
|
|
fn backSurface() Surface {
|
|
return .{
|
|
.pixels = @ptrCast(@alignCast(display.back)),
|
|
.stride = display.pitch / 4, // pitch is bytes; a 32-bpp row is pitch/4 pixels
|
|
.width = display.width,
|
|
.height = display.height,
|
|
};
|
|
}
|
|
|
|
fn layerScreenRect(l: *const Layer) Rect {
|
|
return .{ .x = l.x, .y = l.y, .w = @intCast(l.surface.width), .h = @intCast(l.surface.height) };
|
|
}
|
|
|
|
/// Add `r` (screen coordinates) to the pending damage, clipped to the screen.
|
|
fn addDamage(r: Rect) void {
|
|
damage = damage.unite(r.intersect(screenRect()));
|
|
}
|
|
|
|
// --- layer operations (called from onMessage and the self-check) ------------
|
|
|
|
fn freeLayer() ?u32 {
|
|
for (&layers, 0..) |*l, i| {
|
|
if (!l.used) return @intCast(i);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// A used layer by id, or null if the id is out of range or free.
|
|
fn layerAt(id: u32) ?*Layer {
|
|
if (id >= maximum_layers or !layers[id].used) return null;
|
|
return &layers[id];
|
|
}
|
|
|
|
fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32, visible: bool) ?u32 {
|
|
if (w == 0 or h == 0) return null;
|
|
const slot = freeLayer() orelse return null;
|
|
const len = @as(usize, w) * h * 4;
|
|
const base = system.mmap(len, system.PROT_READ | system.PROT_WRITE);
|
|
if (system.mmapFailed(base)) return null;
|
|
layers[slot] = .{
|
|
.used = true,
|
|
.x = x,
|
|
.y = y,
|
|
.z = z,
|
|
.visible = visible,
|
|
.surface = .{ .pixels = @ptrFromInt(base), .stride = w, .width = w, .height = h },
|
|
.surface_len = len,
|
|
};
|
|
return slot;
|
|
}
|
|
|
|
fn fillLayer(id: u32, local: Rect, colour: u32) bool {
|
|
const l = layerAt(id) orelse return false;
|
|
compositor.fillRect(l.surface, local, colour);
|
|
// Damage in screen space = the fill, translated by the layer origin, within the layer.
|
|
const screen = Rect{ .x = l.x + local.x, .y = l.y + local.y, .w = local.w, .h = local.h };
|
|
addDamage(screen.intersect(layerScreenRect(l)));
|
|
return true;
|
|
}
|
|
|
|
fn blitLayer(id: u32, x: i32, y: i32, w: u32, h: u32, pixels: []const u8) bool {
|
|
const l = layerAt(id) orelse return false;
|
|
compositor.blitTile(l.surface, x, y, pixels, w, h);
|
|
const screen = Rect{ .x = l.x + x, .y = l.y + y, .w = @intCast(w), .h = @intCast(h) };
|
|
addDamage(screen.intersect(layerScreenRect(l)));
|
|
return true;
|
|
}
|
|
|
|
fn configureLayer(id: u32, x: i32, y: i32, z: u32, visible: bool) bool {
|
|
const l = layerAt(id) orelse return false;
|
|
addDamage(layerScreenRect(l)); // the old footprint must repaint
|
|
l.x = x;
|
|
l.y = y;
|
|
l.z = z;
|
|
l.visible = visible;
|
|
addDamage(layerScreenRect(l)); // and the new one
|
|
return true;
|
|
}
|
|
|
|
fn destroyLayer(id: u32) bool {
|
|
const l = layerAt(id) orelse return false;
|
|
addDamage(layerScreenRect(l));
|
|
_ = system.munmap(@intFromPtr(l.surface.pixels), l.surface_len);
|
|
l.* = .{};
|
|
return true;
|
|
}
|
|
|
|
// --- compositing + present --------------------------------------------------
|
|
|
|
/// Repaint the damaged region `clip` of the back buffer: clear it to the background, then
|
|
/// paint every visible layer that overlaps it, bottom to top (ascending z).
|
|
fn compositeInto(clip: Rect) void {
|
|
const back = backSurface();
|
|
compositor.fillRect(back, clip, background);
|
|
|
|
// z-order the used, visible layers (n ≤ 16; a plain insertion sort of indices).
|
|
var order: [maximum_layers]u32 = undefined;
|
|
var n: usize = 0;
|
|
for (layers, 0..) |l, i| {
|
|
if (l.used and l.visible) {
|
|
order[n] = @intCast(i);
|
|
n += 1;
|
|
}
|
|
}
|
|
var a: usize = 1;
|
|
while (a < n) : (a += 1) {
|
|
const key = order[a];
|
|
var b: usize = a;
|
|
while (b > 0 and layers[order[b - 1]].z > layers[key].z) : (b -= 1) order[b] = order[b - 1];
|
|
order[b] = key;
|
|
}
|
|
|
|
for (order[0..n]) |i| {
|
|
const l = layers[i];
|
|
compositor.composite(back, l.x, l.y, l.surface, clip);
|
|
}
|
|
}
|
|
|
|
/// Stream the damaged rectangle from the cacheable back buffer to the write-combining
|
|
/// front buffer, row by row (sequential writes — what WC memory wants; we never read the
|
|
/// front buffer). Only the visible width of each row is touched.
|
|
fn flushRect(rect: Rect) void {
|
|
const c = rect.intersect(screenRect());
|
|
if (c.isEmpty()) return;
|
|
var y: i32 = c.y;
|
|
while (y < c.bottom()) : (y += 1) {
|
|
const off = @as(usize, @intCast(y)) * display.pitch;
|
|
const src: [*]const u32 = @ptrCast(@alignCast(display.back + off));
|
|
const dst: [*]volatile u32 = @ptrCast(@alignCast(display.front + off));
|
|
var x: i32 = c.x;
|
|
while (x < c.right()) : (x += 1) dst[@intCast(x)] = src[@intCast(x)];
|
|
}
|
|
}
|
|
|
|
/// Composite and flush the accumulated damage, then clear it. A no-op when nothing is
|
|
/// dirty. The frame counter advances regardless, so callers can name frames.
|
|
fn present() void {
|
|
const dirty = damage.intersect(screenRect());
|
|
if (!dirty.isEmpty()) {
|
|
compositeInto(dirty);
|
|
flushRect(dirty);
|
|
}
|
|
damage = Rect.empty;
|
|
display.frames += 1;
|
|
}
|
|
|
|
// --- startup self-check -----------------------------------------------------
|
|
|
|
/// Prove the compositor wiring on the real framebuffer: two overlapping opaque layers,
|
|
/// composited, must show the top layer in the overlap and the bottom layer outside it.
|
|
/// Exercises the whole path — mmap surfaces, the z-sort, damage, composite into the back
|
|
/// buffer — and reads the composited result back. Cleans up after itself.
|
|
fn selfCheck() void {
|
|
const red = protocol.pack(display.format, 0xC0, 0x20, 0x20);
|
|
const green = protocol.pack(display.format, 0x20, 0xC0, 0x20);
|
|
const bottom = createLayer(100, 100, 80, 80, 0, true) orelse return fail_check("create");
|
|
const top = createLayer(140, 140, 80, 80, 1, true) orelse return fail_check("create");
|
|
_ = fillLayer(bottom, Rect.init(0, 0, 80, 80), red);
|
|
_ = fillLayer(top, Rect.init(0, 0, 80, 80), green);
|
|
present();
|
|
|
|
const back = backSurface();
|
|
const overlap = back.pixels[@as(usize, 150) * back.stride + 150]; // in both layers → top
|
|
const bottom_only = back.pixels[@as(usize, 110) * back.stride + 110]; // bottom only
|
|
|
|
_ = destroyLayer(top);
|
|
_ = destroyLayer(bottom);
|
|
present(); // repaint the self-check region back to the background
|
|
|
|
if (overlap == green and bottom_only == red) {
|
|
_ = system.write("display: compositor self-check ok\n");
|
|
} else {
|
|
_ = system.write("display: compositor self-check FAILED\n");
|
|
}
|
|
}
|
|
|
|
fn fail_check(_: []const u8) void {
|
|
_ = system.write("display: compositor self-check FAILED (setup)\n");
|
|
}
|
|
|
|
// --- service ----------------------------------------------------------------
|
|
|
|
/// The framebuffer node the kernel seeded (`DeviceClass.display`), or null if none.
|
|
fn findDisplay() ?device.DeviceDescriptor {
|
|
const total = device.enumerate(&device_table);
|
|
const n = @min(total, device_table.len);
|
|
for (device_table[0..n]) |d| {
|
|
if (d.class == @intFromEnum(device.DeviceClass.display)) return d;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn initialise(endpoint: ipc.Handle) bool {
|
|
_ = endpoint;
|
|
|
|
// Find the framebuffer, retrying while device discovery catches up with our spawn.
|
|
var tries: u32 = 0;
|
|
const found = while (tries < 100) : (tries += 1) {
|
|
if (findDisplay()) |d| break d;
|
|
system.sleep(50);
|
|
} else {
|
|
_ = system.write("display: no framebuffer device (headless?)\n");
|
|
return false; // clean exit: nothing to drive
|
|
};
|
|
|
|
if (!device.claim(found.id)) {
|
|
_ = system.write("display: could not claim the framebuffer\n");
|
|
return false;
|
|
}
|
|
// Resource 0 is the framebuffer memory window; the kernel maps it write-combining
|
|
// because the resource carries that flag (docs/display-plan.md D1).
|
|
const front_base = device.mmioMap(found.id, 0) orelse {
|
|
_ = system.write("display: could not map the framebuffer\n");
|
|
return false;
|
|
};
|
|
|
|
const geometry = found.display;
|
|
const size = @as(usize, geometry.height) * geometry.pitch;
|
|
const back_base = system.mmap(size, system.PROT_READ | system.PROT_WRITE);
|
|
if (system.mmapFailed(back_base)) {
|
|
_ = system.write("display: could not allocate the back buffer\n");
|
|
return false;
|
|
}
|
|
|
|
display = .{
|
|
.device_id = found.id,
|
|
.front = @ptrFromInt(front_base),
|
|
.back = @ptrFromInt(back_base),
|
|
.width = geometry.width,
|
|
.height = geometry.height,
|
|
.pitch = geometry.pitch,
|
|
.format = geometry.format,
|
|
};
|
|
background = protocol.pack(display.format, 0x20, 0x30, 0x48); // a dark slate wallpaper
|
|
|
|
// Clear the whole screen through the back buffer → present path (double buffering:
|
|
// no direct-to-LFB drawing).
|
|
addDamage(screenRect());
|
|
present();
|
|
|
|
var line: [96]u8 = undefined;
|
|
_ = system.write(std.fmt.bufPrint(&line, "display: online {d}x{d} pitch {d} format {d}\n", .{
|
|
display.width, display.height, display.pitch, display.format,
|
|
}) catch "display: online\n");
|
|
_ = system.write("display: presented frame 0\n");
|
|
|
|
selfCheck();
|
|
return true;
|
|
}
|
|
|
|
fn writeReply(reply: []u8, value: protocol.Reply) usize {
|
|
const bytes = std.mem.asBytes(&value);
|
|
@memcpy(reply[0..bytes.len], bytes);
|
|
return bytes.len;
|
|
}
|
|
|
|
fn ok(reply: []u8) usize {
|
|
return writeReply(reply, .{ .status = 0 });
|
|
}
|
|
|
|
fn fail(reply: []u8) usize {
|
|
return writeReply(reply, .{ .status = -1 });
|
|
}
|
|
|
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize {
|
|
_ = sender;
|
|
_ = capability;
|
|
if (message.len < protocol.request_size) return fail(reply);
|
|
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
|
|
const payload = message[protocol.request_size..];
|
|
// Switch on the raw operation value — an out-of-range one must fail cleanly, not
|
|
// panic an `@enumFromInt`.
|
|
switch (request.operation) {
|
|
@intFromEnum(protocol.Operation.info) => return writeReply(reply, .{
|
|
.status = 0,
|
|
.width = display.width,
|
|
.height = display.height,
|
|
.pitch = display.pitch,
|
|
.format = display.format,
|
|
}),
|
|
@intFromEnum(protocol.Operation.create_layer) => {
|
|
// x/y are signed coordinates carried in the u32 wire fields — reinterpret the
|
|
// bits (@bitCast), don't range-check (@intCast) which a negative would fail.
|
|
const slot = createLayer(@bitCast(request.x), @bitCast(request.y), request.width, request.height, request.z, request.visible != 0) orelse return fail(reply);
|
|
return writeReply(reply, .{ .status = 0, .layer = slot });
|
|
},
|
|
@intFromEnum(protocol.Operation.configure_layer) => {
|
|
return if (configureLayer(request.layer, @bitCast(request.x), @bitCast(request.y), request.z, request.visible != 0)) ok(reply) else fail(reply);
|
|
},
|
|
@intFromEnum(protocol.Operation.destroy_layer) => {
|
|
return if (destroyLayer(request.layer)) ok(reply) else fail(reply);
|
|
},
|
|
@intFromEnum(protocol.Operation.fill_rect) => {
|
|
const local = Rect.init(@bitCast(request.x), @bitCast(request.y), @intCast(request.width), @intCast(request.height));
|
|
return if (fillLayer(request.layer, local, request.colour)) ok(reply) else fail(reply);
|
|
},
|
|
@intFromEnum(protocol.Operation.blit_tile) => {
|
|
return if (blitLayer(request.layer, @bitCast(request.x), @bitCast(request.y), request.width, request.height, payload)) ok(reply) else fail(reply);
|
|
},
|
|
@intFromEnum(protocol.Operation.damage) => {
|
|
const l = layerAt(request.layer) orelse return fail(reply);
|
|
const screen = Rect{ .x = l.x + @as(i32, @bitCast(request.x)), .y = l.y + @as(i32, @bitCast(request.y)), .w = @intCast(request.width), .h = @intCast(request.height) };
|
|
addDamage(screen.intersect(layerScreenRect(l)));
|
|
return ok(reply);
|
|
},
|
|
@intFromEnum(protocol.Operation.present) => {
|
|
present();
|
|
return ok(reply);
|
|
},
|
|
else => return fail(reply),
|
|
}
|
|
}
|
|
|
|
pub fn main() void {
|
|
runtime.service.run(protocol.message_maximum, .{
|
|
.service = .display,
|
|
.init = initialise,
|
|
.on_message = onMessage,
|
|
});
|
|
}
|
|
|
|
pub const panic = runtime.panic;
|
|
comptime {
|
|
_ = &runtime.start._start;
|
|
}
|