display: framebuffer handoff primitive + service design (D1)
Kick off the display service track (docs/display.md, docs/display-plan.md): a
user-space compositor that owns the framebuffer. GOP and the PCI display device
are two views of one controller; GOP dies at ExitBootServices, so the portable
base is the boot-handoff linear framebuffer.
D1 makes that framebuffer reachable from user space over the existing device
claim/mmio_map path rather than a bespoke syscall:
- device-abi: a `display` DeviceClass, a DisplayInfo{w,h,pitch,format} on the
descriptor, and a flags field on resources with a write-combining bit.
- devices-broker: seedDisplay() publishes the loader's framebuffer as a
root-level `display` node (one WC-flagged memory resource); kmain seeds it
after discovery. displayDevice()/displayClaimed() track the claim.
- paging/mmio_map: mapUserDeviceInto gains a write_combining bool — a WC-flagged
resource maps through PAT entry 4 instead of strong-uncacheable (an
uncacheable framebuffer blit is glacial).
- console: falls silent while a display service holds the framebuffer, and is
forced back on by the panic/exception paths.
Gate: the `display` kernel test asserts the seeded node's shape and that the
claim + mmio_map leaf is genuinely write-combining (PAT bit set, PCD/PWT clear).
Regression-checked discovery/ioport/claim-release/supervision/device-list/
device-manager with the +1 device in the table.
This commit is contained in:
+73
-2
@@ -95,6 +95,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
||||
iommuTest();
|
||||
} else if (eql(case, "ioport")) {
|
||||
ioPortTest();
|
||||
} else if (eql(case, "display")) {
|
||||
displayTest(boot_information);
|
||||
} else if (eql(case, "clock")) {
|
||||
clockTest();
|
||||
} else if (eql(case, "smp")) {
|
||||
@@ -2627,8 +2629,8 @@ fn ioPassTest() void {
|
||||
result();
|
||||
return;
|
||||
};
|
||||
// Map it the way mmio_map does (device grant), then tear the space down.
|
||||
architecture.mapUserDeviceInto(aspace, process.device_arena_base, frame, abi.page_size);
|
||||
// Map it the way mmio_map does (device grant, strong-uncacheable), then tear the space down.
|
||||
architecture.mapUserDeviceInto(aspace, process.device_arena_base, frame, abi.page_size, false);
|
||||
architecture.destroyAddressSpace(aspace);
|
||||
|
||||
// The page tables were reclaimed; the device-granted frame must not have been.
|
||||
@@ -2638,6 +2640,75 @@ fn ioPassTest() void {
|
||||
result();
|
||||
}
|
||||
|
||||
/// D1 — the framebuffer handoff primitive. The kernel seeds the loader's framebuffer as
|
||||
/// a claimable `display` device with a write-combining `memory` resource; a display
|
||||
/// service reaches it over the ordinary claim + mmio_map path. Prove the whole chain:
|
||||
/// the node is present and correctly shaped, it maps, and — the point of D1 — the
|
||||
/// mapping is genuinely write-combining, not the strong-uncacheable default that would
|
||||
/// make a framebuffer blit glacial.
|
||||
fn displayTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: display\n", .{});
|
||||
const fb = boot_information.framebuffer;
|
||||
if (!fb.present()) {
|
||||
// Headless: nothing to seed. Not a failure of the mechanism, so pass cleanly.
|
||||
log("display: no framebuffer (headless); skipping\n", .{});
|
||||
result();
|
||||
return;
|
||||
}
|
||||
|
||||
// The kernel seeded a display device in kmain, right after devices_broker.init.
|
||||
const display_id = devices_broker.displayDevice() orelse {
|
||||
check("a framebuffer display device was seeded", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
var buffer: [64]device_abi.DeviceDescriptor = undefined;
|
||||
const n = @min(devices_broker.enumerate(&buffer), buffer.len);
|
||||
check("the seeded display id is enumerable", display_id < n);
|
||||
if (display_id >= n) {
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const d = buffer[@intCast(display_id)];
|
||||
|
||||
check("the node is class display", d.class == @intFromEnum(device_abi.DeviceClass.display));
|
||||
check("it carries the framebuffer geometry", d.display.width == fb.width and d.display.height == fb.height and d.display.pitch == fb.pitch);
|
||||
check("it has exactly one resource", d.resource_count == 1);
|
||||
const r = d.resources[0];
|
||||
check("that resource is a memory window", r.kind == @intFromEnum(device_abi.ResourceKind.memory));
|
||||
check("it spans the whole framebuffer", r.start == fb.base and r.len == @as(u64, fb.height) * fb.pitch);
|
||||
check("it is flagged write-combining", (r.flags & device_abi.resource_flag_write_combining) != 0);
|
||||
|
||||
// Walk the real claim + map path a display service would, into a throwaway address
|
||||
// space, and confirm the leaf's cache type. We never run this space (no CR3 load) —
|
||||
// we only read back the page-table entries — so aliasing the same physical page at
|
||||
// two cache types below is inert.
|
||||
const aspace = architecture.createAddressSpace() orelse {
|
||||
check("created a fresh address space", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
defer architecture.destroyAddressSpace(aspace);
|
||||
|
||||
const page_base = fb.base & ~@as(u64, abi.page_size - 1);
|
||||
architecture.mapUserDeviceInto(aspace, process.device_arena_base, page_base, abi.page_size, true);
|
||||
check(
|
||||
"the framebuffer maps write-combining (PAT entry 4: PAT bit set, PCD/PWT clear)",
|
||||
architecture.userLeafIsWriteCombining(aspace, process.device_arena_base) == true,
|
||||
);
|
||||
|
||||
// Regression guard: the strong-uncacheable default is still that, so WC is a real
|
||||
// choice the flag makes, not the only behaviour.
|
||||
architecture.mapUserDeviceInto(aspace, process.device_arena_base + abi.page_size, page_base, abi.page_size, false);
|
||||
check(
|
||||
"a register window still maps strong-uncacheable",
|
||||
architecture.userLeafIsWriteCombining(aspace, process.device_arena_base + abi.page_size) == false,
|
||||
);
|
||||
|
||||
log("display: mapped {d}x{d} pitch {d} (write-combining)\n", .{ fb.width, fb.height, fb.pitch });
|
||||
result();
|
||||
}
|
||||
|
||||
fn faultInvalidOpcode() void {
|
||||
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
|
||||
asm volatile ("ud2");
|
||||
|
||||
Reference in New Issue
Block a user