display: the compositor service — claim, double-buffer, present (D2)
Stand up /system/services/display: a ring-3 process that claims the framebuffer D1 seeded, maps it write-combining as the front buffer, allocates a cacheable back buffer, and presents composed frames. The GUI track's compositor, reached by name over ServiceId.display (= 9). - protocol.zig: the display wire protocol (info/create_layer/configure_layer/ destroy_layer/fill_rect/blit_tile/damage/present). `info` and a whole-screen `present` are live; the layer ops fail-stub until D3. - display.zig: enumerate -> claim -> mmio_map(WC) the LFB, mmap a cacheable back buffer, clear it and present it (proving the double-buffer path), then serve. - runtime.display + barrel exports (display, display_protocol): a cached `.display` client with info()/present(), the runtime.block shape. - init spawns "display" in boot_services; build.zig wires the protocol onto the runtime, builds the exe, packs it into the initial-ramdisk, installs it. mmap fix the back buffer forced: systemMmap was capped at 256 pages (1 MiB) by a fixed kernel-stack scratch array. Rewrote it to map page-by-page with rollback (no array) and raised the cap to 8192 pages (32 MiB) — enough for a 4K back buffer. A real limitation met. Gate: `python3 test/qemu_test.py display-service` matches the service's own serial heartbeats (display: online WxH / presented frame 0), printed only after the full claim -> WC-map -> back-buffer -> present chain. Regression-checked usermem, heap, init, and D1's display.
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
//! /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 into a **cacheable back buffer**, and
|
||||
//! presents finished frames — the GUI track's compositor, the sibling of the input
|
||||
//! service. It is reached by name over `ServiceId.display`.
|
||||
//!
|
||||
//! This is the D2 skeleton: it comes up, claims + maps the framebuffer, allocates the
|
||||
//! back buffer, and proves the double-buffer path by clearing the back buffer and
|
||||
//! presenting it. The layer stack, damage tracking, and per-layer drawing arrive in D3;
|
||||
//! for now `info` and a whole-screen `present` are the live operations.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
|
||||
const protocol = runtime.display_protocol;
|
||||
const ipc = runtime.ipc;
|
||||
const system = runtime.system;
|
||||
const device = runtime.device;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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,
|
||||
};
|
||||
|
||||
// Prove the pipeline end to end: compose a cleared frame in the back buffer, then
|
||||
// present it to the screen. Nothing is drawn directly to the LFB.
|
||||
clear(0x0020_3048); // a dark slate; exact channel order is a D3 concern
|
||||
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");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Fill the whole back buffer with `colour`. Cacheable memory, so this is fast; touch
|
||||
/// only the visible width, stepping rows by `pitch` (which may exceed width*4).
|
||||
fn clear(colour: u32) void {
|
||||
var y: u32 = 0;
|
||||
while (y < display.height) : (y += 1) {
|
||||
const row: [*]u32 = @ptrCast(@alignCast(display.back + @as(usize, y) * display.pitch));
|
||||
var x: u32 = 0;
|
||||
while (x < display.width) : (x += 1) row[x] = colour;
|
||||
}
|
||||
}
|
||||
|
||||
/// Whole-screen present: stream the back buffer to the write-combining front buffer, row
|
||||
/// by row. Sequential writes are what WC memory wants; we never read the front buffer.
|
||||
/// (D3 replaces this with a damage-driven present that copies only changed rectangles.)
|
||||
fn present() void {
|
||||
var y: u32 = 0;
|
||||
while (y < display.height) : (y += 1) {
|
||||
const src: [*]const u32 = @ptrCast(@alignCast(display.back + @as(usize, y) * display.pitch));
|
||||
const dst: [*]volatile u32 = @ptrCast(@alignCast(display.front + @as(usize, y) * display.pitch));
|
||||
var x: u32 = 0;
|
||||
while (x < display.width) : (x += 1) dst[x] = src[x];
|
||||
}
|
||||
display.frames += 1;
|
||||
}
|
||||
|
||||
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]);
|
||||
// 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.present) => {
|
||||
present();
|
||||
return ok(reply);
|
||||
},
|
||||
// The layer stack and per-layer drawing land in D3; until then those operations
|
||||
// are unimplemented rather than silently accepted.
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user