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:
+21
-14
@@ -81,9 +81,11 @@ pub const device_arena_end: u64 = device_arena_base + (4 << 30);
|
||||
pub const dma_arena_base: u64 = 0x0000_7200_0000_0000;
|
||||
pub const dma_arena_end: u64 = dma_arena_base + (256 << 20); // 256 MiB per process
|
||||
|
||||
/// Largest single `mmap` grant, in pages (1 MiB). The user heap grows in small
|
||||
/// chunks, so this bound is generous; it also caps the frame scratch array below.
|
||||
const maximum_mmap_pages = 256;
|
||||
/// Largest single `mmap` grant, in pages (32 MiB). Big enough for a display service's
|
||||
/// back buffer at up to 4K (3840x2160x4 ≈ 8100 pages); the user heap otherwise grows in
|
||||
/// small chunks. `systemMmap` maps page by page with rollback, so this is only a sanity
|
||||
/// bound (and an overflow guard on the page count), not the size of any scratch array.
|
||||
const maximum_mmap_pages = 8192;
|
||||
|
||||
/// Ceiling on a process's argv entries, including argv[0]. Arguments are spawn
|
||||
/// parameters ("you are the driver for device 12"), not bulk data — IPC carries
|
||||
@@ -1038,21 +1040,26 @@ fn systemMmap(state: *architecture.CpuState) void {
|
||||
const base = t.heap_next;
|
||||
if (base + pages * page_size > heap_arena_end) return fail(state); // arena exhausted
|
||||
|
||||
// Reserve all frames up front so a mid-way exhaustion rolls back cleanly
|
||||
// (no partially-mapped grant leaks into the address space).
|
||||
var frames: [maximum_mmap_pages]u64 = undefined;
|
||||
var got: usize = 0;
|
||||
while (got < pages) : (got += 1) {
|
||||
frames[got] = pmm.alloc() orelse {
|
||||
for (frames[0..got]) |f| pmm.free(f);
|
||||
// Map page by page. On mid-way frame exhaustion, roll back the pages already mapped
|
||||
// (unmap + free) so no partial grant leaks into the address space — the same
|
||||
// all-or-nothing guarantee as before, but without a fixed scratch array, so the
|
||||
// per-call size can be a multi-MiB framebuffer.
|
||||
var mapped: usize = 0;
|
||||
while (mapped < pages) : (mapped += 1) {
|
||||
const frame = pmm.alloc() orelse {
|
||||
var i: usize = 0;
|
||||
while (i < mapped) : (i += 1) {
|
||||
const va = base + i * page_size;
|
||||
if (architecture.translate(t.aspace, va)) |physical| {
|
||||
architecture.unmapUserPageInto(t.aspace, va);
|
||||
pmm.free(physical);
|
||||
}
|
||||
}
|
||||
return fail(state);
|
||||
};
|
||||
}
|
||||
|
||||
for (frames[0..pages], 0..) |frame, i| {
|
||||
const destination: [*]u8 = @ptrFromInt(boot_handoff.physicalToVirtual(frame));
|
||||
@memset(destination[0..page_size], 0); // hand out zeroed memory
|
||||
architecture.mapUserPageInto(t.aspace, base + i * page_size, frame, true, false); // RW + NX
|
||||
architecture.mapUserPageInto(t.aspace, base + mapped * page_size, frame, true, false); // RW + NX
|
||||
}
|
||||
t.heap_next = base + pages * page_size;
|
||||
architecture.setSystemCallResult(state, base);
|
||||
|
||||
@@ -97,6 +97,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
||||
ioPortTest();
|
||||
} else if (eql(case, "display")) {
|
||||
displayTest(boot_information);
|
||||
} else if (eql(case, "display-service")) {
|
||||
displayServiceTest(boot_information);
|
||||
} else if (eql(case, "clock")) {
|
||||
clockTest();
|
||||
} else if (eql(case, "smp")) {
|
||||
@@ -2307,6 +2309,41 @@ fn inputTest(boot_information: *const BootInformation) void {
|
||||
result();
|
||||
}
|
||||
|
||||
/// D2 — the display service comes up. Spawn it from the initial_ramdisk; it claims the
|
||||
/// framebuffer the kernel seeded (D1), maps it write-combining, allocates a cacheable
|
||||
/// back buffer, and proves the double-buffer path by clearing that buffer and presenting
|
||||
/// it. Its `display: online WxH` + `display: presented frame 0` heartbeats are the
|
||||
/// markers — seeing them proves a user-space compositor took the framebuffer and pushed
|
||||
/// a whole composed frame to the screen, without ever drawing straight to the LFB.
|
||||
fn displayServiceTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: display-service\n", .{});
|
||||
if (boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over an initial_ramdisk", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||
const rd = initial_ramdisk.Reader.init(image) orelse {
|
||||
check("initial_ramdisk image is valid", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
|
||||
// Spawn the compositor and hand it the core. Its own serial heartbeats — `display:
|
||||
// online WxH` and `display: presented frame 0` — are what the harness matches (it
|
||||
// reads serial directly, like the fault cases). We don't poll for them in-kernel: a
|
||||
// single service that comes up and blocks doesn't reschedule this bring-up context
|
||||
// (there is no other runnable task to bounce control back through), so the honest
|
||||
// observation point is the service's output itself, not a check() proxy here.
|
||||
if (!spawnNamed(rd, "display")) {
|
||||
log("display-service: could not spawn the display service\n", .{});
|
||||
result();
|
||||
return;
|
||||
}
|
||||
scheduler.setPriority(1); // below the service, so it runs and comes up first
|
||||
while (true) scheduler.yield();
|
||||
}
|
||||
|
||||
/// Process arguments, end to end: spawn args-echo bare (its argv[0] is the
|
||||
/// initial-ramdisk name). Instance 1 sees argc == 1 and respawns itself through
|
||||
/// `system_spawn` with the extra arguments "alpha beta-42" — the syscall argument
|
||||
|
||||
Reference in New Issue
Block a user