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:
@@ -38,6 +38,13 @@ pub const DeviceClass = enum(u32) {
|
||||
/// resources; the (class, subclass, protocol) triple that says what it is
|
||||
/// travels in the bus report's identity, not here.
|
||||
usb_device,
|
||||
/// A scanout framebuffer: a linear region of pixel memory the display service
|
||||
/// claims and maps. Unlike the other classes this one is not firmware-discovered
|
||||
/// — the kernel seeds it from the loader's [[boot-handoff]] framebuffer
|
||||
/// (`devices_broker.seedDisplay`). Its one `memory` resource is the framebuffer,
|
||||
/// flagged write-combining; the geometry to interpret it travels in
|
||||
/// `DeviceDescriptor.display`.
|
||||
display,
|
||||
unknown,
|
||||
};
|
||||
|
||||
@@ -59,10 +66,39 @@ pub const ResourceDescriptor = extern struct {
|
||||
kind: u64, // a ResourceKind value
|
||||
start: u64,
|
||||
len: u64,
|
||||
/// A bitmask of `resource_flag_*` hints. Zero for a plain register/RAM window;
|
||||
/// the kernel reads it when it maps the resource. Defaulted so every existing
|
||||
/// literal (which never set flags) keeps compiling and lays out identically.
|
||||
flags: u64 = 0,
|
||||
};
|
||||
|
||||
/// `ResourceDescriptor.flags`: map this `memory` resource **write-combining** rather
|
||||
/// than strong-uncacheable — for a framebuffer, where batched bursts to pixel memory
|
||||
/// are the whole point (an uncacheable framebuffer blit is glacial). See
|
||||
/// `mmio_map` (system/kernel/process.zig) and `setupPat` (…/x86_64/paging.zig).
|
||||
pub const resource_flag_write_combining: u64 = 1 << 0;
|
||||
|
||||
pub const maximum_device_resources = 8;
|
||||
|
||||
/// The byte order of a display's pixels — mirrors the loader's `PixelFormat`
|
||||
/// ([[boot-handoff]]) with the same numeric values, but lives here so user space
|
||||
/// (which must never import the loader↔kernel handoff) can name it. Only the two
|
||||
/// linear 32-bpp layouts a console can paint into exist; see docs/gop.md.
|
||||
pub const DisplayFormat = enum(u32) {
|
||||
rgbx = 0, // byte 0 = Red, 1 = Green, 2 = Blue, 3 = reserved
|
||||
bgrx = 1, // byte 0 = Blue, 1 = Green, 2 = Red, 3 = reserved
|
||||
};
|
||||
|
||||
/// The geometry of a `display` device's framebuffer, carried in its descriptor so a
|
||||
/// claiming driver knows how to interpret the pixel bytes its `memory` resource maps.
|
||||
/// `pitch` is bytes per row (may exceed `width * 4`; see docs/framebuffer.md).
|
||||
pub const DisplayInfo = extern struct {
|
||||
width: u32 = 0, // visible pixels per row
|
||||
height: u32 = 0, // visible rows
|
||||
pitch: u32 = 0, // bytes from one row's start to the next
|
||||
format: u32 = 0, // a DisplayFormat value
|
||||
};
|
||||
|
||||
/// `DeviceDescriptor.parent` for a device with no parent — a root of the device tree.
|
||||
pub const no_parent: u64 = ~@as(u64, 0);
|
||||
|
||||
@@ -92,4 +128,9 @@ pub const DeviceDescriptor = extern struct {
|
||||
resource_count: u64,
|
||||
hid: [8]u8,
|
||||
resources: [maximum_device_resources]ResourceDescriptor,
|
||||
// Framebuffer geometry, meaningful only when `class` is `DeviceClass.display`
|
||||
// (zeroed otherwise). Kept here — a class-specific field on the shared descriptor —
|
||||
// the same way `pci_class` is meaningful only for `pci_device` and `hid` only for
|
||||
// `acpi_device`.
|
||||
display: DisplayInfo = .{},
|
||||
};
|
||||
|
||||
@@ -168,10 +168,18 @@ pub fn mapUserPageInto(root: u64, virtual: u64, physical: u64, writable: bool, e
|
||||
paging.mapUserInto(root, virtual, physical, writable, executable);
|
||||
}
|
||||
|
||||
/// Map a device MMIO window into address space `root`: strong-uncacheable, RW+NX,
|
||||
/// and marked so teardown won't free the MMIO frames as RAM. For IO passthrough.
|
||||
pub fn mapUserDeviceInto(root: u64, virtual: u64, physical: u64, len: u64) void {
|
||||
paging.mapUserDeviceInto(root, virtual, physical, len);
|
||||
/// Map a device MMIO window into address space `root`: RW+NX, and marked so teardown
|
||||
/// won't free the MMIO frames as RAM. `write_combining` picks the cache type —
|
||||
/// false = strong-uncacheable (registers), true = write-combining (a framebuffer).
|
||||
/// For IO passthrough.
|
||||
pub fn mapUserDeviceInto(root: u64, virtual: u64, physical: u64, len: u64, write_combining: bool) void {
|
||||
paging.mapUserDeviceInto(root, virtual, physical, len, write_combining);
|
||||
}
|
||||
|
||||
/// Is the user leaf mapping `virtual` in address space `root` write-combining? Null if
|
||||
/// unmapped. For tests verifying the framebuffer map's cache type.
|
||||
pub fn userLeafIsWriteCombining(root: u64, virtual: u64) ?bool {
|
||||
return paging.leafIsWriteCombining(root, virtual);
|
||||
}
|
||||
|
||||
/// Map coherent DMA RAM into address space `root`: strong-uncacheable, RW+NX, but
|
||||
|
||||
@@ -320,8 +320,12 @@ pub fn mapUserInto(pml4: u64, virtual: u64, physical: u64, writable_page: bool,
|
||||
/// RAM allocator (`freeSubtree`). RW + NX; the caller places `virtual` in a
|
||||
/// user-exclusive range (PML4[225]). Both `virtual` and `physical` are page-aligned by
|
||||
/// the caller; a sub-page `physical` offset is the caller's to re-apply.
|
||||
pub fn mapUserDeviceInto(pml4: u64, virtual: u64, physical: u64, len: u64) void {
|
||||
const flags: u64 = present | user | writable | no_execute | pcd | pwt | device_grant;
|
||||
pub fn mapUserDeviceInto(pml4: u64, virtual: u64, physical: u64, len: u64, write_combining: bool) void {
|
||||
// Registers are strong-uncacheable (PCD|PWT). A framebuffer instead wants
|
||||
// write-combining — the PAT bit (bit 7 in a 4 KiB PTE) with PCD=PWT=0 selects PAT
|
||||
// entry 4, which `setupPat` programs to WC — so pixel writes batch into bursts.
|
||||
const cache: u64 = if (write_combining) pte_pat else (pcd | pwt);
|
||||
const flags: u64 = present | user | writable | no_execute | device_grant | cache;
|
||||
const first = physical & ~@as(u64, page_size - 1);
|
||||
const last = (physical + (if (len == 0) 1 else len) - 1) & ~@as(u64, page_size - 1);
|
||||
var off: u64 = 0;
|
||||
@@ -362,6 +366,33 @@ pub fn mapUserDmaInto(pml4: u64, virtual: u64, physical: u64, len: u64) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// The raw leaf entry mapping `virtual` in the address space rooted at `pml4`, or null
|
||||
/// if any level of the walk is absent. **Read-only** — never allocates or descends into
|
||||
/// a missing table (unlike the `map*` paths' `descendUser`). Stops at the first huge
|
||||
/// leaf. For tests and introspection that need a page's actual flag bits.
|
||||
pub fn leafEntryOf(pml4: u64, virtual: u64) ?u64 {
|
||||
const l4 = tableAt(pml4)[(virtual >> 39) & 0x1FF];
|
||||
if (l4 & present == 0) return null;
|
||||
const l3 = tableAt(l4 & address_mask)[(virtual >> 30) & 0x1FF];
|
||||
if (l3 & present == 0) return null;
|
||||
if (l3 & page_size_bit != 0) return l3; // 1 GiB leaf
|
||||
const l2 = tableAt(l3 & address_mask)[(virtual >> 21) & 0x1FF];
|
||||
if (l2 & present == 0) return null;
|
||||
if (l2 & page_size_bit != 0) return l2; // 2 MiB leaf
|
||||
const l1 = tableAt(l2 & address_mask)[(virtual >> 12) & 0x1FF];
|
||||
if (l1 & present == 0) return null;
|
||||
return l1;
|
||||
}
|
||||
|
||||
/// Is the 4 KiB leaf mapping `virtual` write-combining — the PAT bit set with PCD and
|
||||
/// PWT clear, which `setupPat` makes PAT entry 4 (WC)? Null if unmapped. The device
|
||||
/// mapping path (`mapUserDeviceInto`) always uses 4 KiB leaves, so bit 7 (`pte_pat`)
|
||||
/// is the PAT selector in play.
|
||||
pub fn leafIsWriteCombining(pml4: u64, virtual: u64) ?bool {
|
||||
const e = leafEntryOf(pml4, virtual) orelse return null;
|
||||
return (e & pte_pat != 0) and (e & pcd == 0) and (e & pwt == 0);
|
||||
}
|
||||
|
||||
/// Create a new address space: a fresh PML4 with an empty user half and the
|
||||
/// kernel's higher half shared in (copying PML4[256..512), whose entries point
|
||||
/// at the kernel's PDPTs — pre-created at init and never restaled, so growth in
|
||||
|
||||
@@ -20,6 +20,12 @@ const boot_handoff = @import("boot-handoff");
|
||||
var con: Console = undefined;
|
||||
var con_present: bool = false;
|
||||
|
||||
/// Set while a user-space display service owns the framebuffer: `write` falls silent so
|
||||
/// the kernel doesn't paint over the compositor. Driven by the display device's
|
||||
/// claim/release (system/kernel/process.zig). The terminal panic/exception paths clear
|
||||
/// it first (`setSuppressed(false)`) — a dying machine's message wins over any display.
|
||||
var suppressed: bool = false;
|
||||
|
||||
/// Set up the console over `fb`, or mark it absent if there's no usable
|
||||
/// framebuffer. Clears the screen when present.
|
||||
pub fn init(fb: boot_handoff.Framebuffer) void {
|
||||
@@ -41,13 +47,20 @@ pub fn present() bool {
|
||||
return con_present;
|
||||
}
|
||||
|
||||
/// Output sink: draw `bytes` on screen. A no-op when no framebuffer is present,
|
||||
/// so it's always safe to call.
|
||||
/// Output sink: draw `bytes` on screen. A no-op when no framebuffer is present, or
|
||||
/// while a display service owns the screen (`suppressed`), so it's always safe to call.
|
||||
pub fn write(bytes: []const u8) void {
|
||||
if (!con_present) return;
|
||||
if (!con_present or suppressed) return;
|
||||
for (bytes) |c| con.putChar(c);
|
||||
}
|
||||
|
||||
/// Quiesce (or resume) the bootstrap console. Set true when a display service claims the
|
||||
/// framebuffer; set false when that claim is released, or by the panic path to force a
|
||||
/// last message onto a screen a (now-irrelevant) service was holding.
|
||||
pub fn setSuppressed(value: bool) void {
|
||||
suppressed = value;
|
||||
}
|
||||
|
||||
/// The console font, embedded at compile time. cp850-8x16, PSF2 format:
|
||||
/// a 32-byte header, then 256 glyphs of 16 bytes each (one byte per 8-pixel
|
||||
/// row). We index glyphs straight by byte value, so ASCII maps 1:1.
|
||||
|
||||
@@ -36,6 +36,11 @@ var devices: [maximum_devices]device_abi.DeviceDescriptor = undefined;
|
||||
var claimed: [maximum_devices]?u32 = .{null} ** maximum_devices; // owner task id, or null
|
||||
var count: usize = 0;
|
||||
|
||||
/// The id of the seeded framebuffer node (`seedDisplay`), or null when the machine
|
||||
/// handed over no framebuffer. Lets the process layer recognise the display claim
|
||||
/// (to quiesce the bootstrap console) without threading the id through every caller.
|
||||
var display_device: ?u64 = null;
|
||||
|
||||
/// Devices discovery found but the table had no room for. Non-zero means the machine
|
||||
/// is bigger than `maximum_devices` and some hardware is simply invisible to drivers —
|
||||
/// which would otherwise be an entirely silent failure. Logged at boot.
|
||||
@@ -45,10 +50,55 @@ pub var dropped: usize = 0;
|
||||
pub fn init(device_tree: *const platform.DeviceTree) void {
|
||||
count = 0;
|
||||
dropped = 0;
|
||||
display_device = null;
|
||||
for (&claimed) |*c| c.* = null;
|
||||
walk(device_tree.root, device_abi.no_parent);
|
||||
}
|
||||
|
||||
/// Publish the loader's framebuffer as a `display` device — a root-level node with one
|
||||
/// write-combining `memory` resource over the linear framebuffer and its geometry in
|
||||
/// `.display`. The framebuffer is *not* firmware-discovered (it rides the
|
||||
/// [[boot-handoff]], not the device tree), so it is seeded explicitly, after `init`.
|
||||
/// Returns the new device id, or null when there is no framebuffer (headless) or the
|
||||
/// table is full. Idempotent-ish: only ever call once per boot.
|
||||
pub fn seedDisplay(base: u64, width: u32, height: u32, pitch: u32, format: u32) ?u64 {
|
||||
if (base == 0 or width == 0 or height == 0) return null; // headless
|
||||
if (count >= maximum_devices) {
|
||||
dropped += 1;
|
||||
return null;
|
||||
}
|
||||
var d = std.mem.zeroes(device_abi.DeviceDescriptor);
|
||||
d.id = count;
|
||||
d.parent = device_abi.no_parent;
|
||||
d.class = @intFromEnum(device_abi.DeviceClass.display);
|
||||
d.pci_class = device_abi.no_pci_class;
|
||||
d.resource_count = 1;
|
||||
d.resources[0] = .{
|
||||
.kind = @intFromEnum(device_abi.ResourceKind.memory),
|
||||
.start = base,
|
||||
.len = @as(u64, height) * pitch,
|
||||
.flags = device_abi.resource_flag_write_combining,
|
||||
};
|
||||
d.display = .{ .width = width, .height = height, .pitch = pitch, .format = format };
|
||||
devices[count] = d;
|
||||
display_device = d.id;
|
||||
count += 1;
|
||||
return d.id;
|
||||
}
|
||||
|
||||
/// The id of the seeded framebuffer device, or null when none was seeded.
|
||||
pub fn displayDevice() ?u64 {
|
||||
return display_device;
|
||||
}
|
||||
|
||||
/// Whether the framebuffer device is currently claimed by some process. The bootstrap
|
||||
/// console uses this (via the process layer) to fall silent while a display service
|
||||
/// owns the screen, and to resume if that service dies and its claim is released.
|
||||
pub fn displayClaimed() bool {
|
||||
const id = display_device orelse return false;
|
||||
return ownerOf(id) != null;
|
||||
}
|
||||
|
||||
/// Record `node` (unless it's the synthetic root) and recurse, threading the id we
|
||||
/// assigned it down to its children as their parent.
|
||||
fn walk(node: *platform.Device, parent_id: u64) void {
|
||||
|
||||
@@ -195,6 +195,13 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
||||
log.print("/system/kernel: WARNING {d} device(s) dropped — table full\n", .{devices_broker.dropped});
|
||||
}
|
||||
|
||||
// Publish the loader's framebuffer as a claimable `display` device, so a
|
||||
// user-space display service can take it over the same claim + mmio_map path as
|
||||
// any other hardware (it is not firmware-discovered; it rides the boot handoff).
|
||||
if (devices_broker.seedDisplay(fb.base, fb.width, fb.height, fb.pitch, @intFromEnum(fb.format))) |display_id| {
|
||||
log.print("/system/kernel: framebuffer device {d} seeded ({d}x{d}, pitch {d}, write-combining)\n", .{ display_id, fb.width, fb.height, fb.pitch });
|
||||
}
|
||||
|
||||
// Install the device-IRQ trampolines, so a driver's irq_bind has vectors to
|
||||
// land on. Every line stays masked until something binds it (ioapic.init).
|
||||
irq.init();
|
||||
@@ -479,6 +486,9 @@ fn onException(state: *const architecture.CpuState) noreturn {
|
||||
}
|
||||
|
||||
log.checkpoint(cp_exception);
|
||||
// The machine is going down: force the console back on even if a display service
|
||||
// was holding the framebuffer, so the exception actually reaches the screen.
|
||||
console.setSuppressed(false);
|
||||
const core = scheduler.currentCpuIndex();
|
||||
// A fault is user-facing enough to paint on screen too (via statusPrint), on
|
||||
// top of the diagnostic log.
|
||||
@@ -501,6 +511,7 @@ pub const panic = std.debug.FullPanic(struct {
|
||||
_ = first_trace_address;
|
||||
log.checkpoint(cp_panic);
|
||||
log.recordPanic(message);
|
||||
console.setSuppressed(false); // a panic outranks any display service holding the screen
|
||||
status("\nKERNEL PANIC: ");
|
||||
status(message);
|
||||
status("\n");
|
||||
|
||||
@@ -28,6 +28,7 @@ const parameters = @import("parameters");
|
||||
const architecture = @import("architecture");
|
||||
const pmm = @import("pmm.zig");
|
||||
const scheduler = @import("scheduler.zig");
|
||||
const console = @import("console.zig");
|
||||
const sync = @import("sync.zig");
|
||||
const ipc = @import("ipc-synchronous.zig");
|
||||
const devices_broker = @import("devices-broker.zig");
|
||||
@@ -301,12 +302,19 @@ fn systemDeviceEnumerate(state: *architecture.CpuState) void {
|
||||
|
||||
/// device_claim(id) -> 0/-1: take exclusive ownership of a device for this process.
|
||||
fn systemDeviceClaim(state: *architecture.CpuState) void {
|
||||
const device_id = architecture.systemCallArg(state, 0);
|
||||
const claim_flags = sync.enter();
|
||||
defer sync.leave(claim_flags);
|
||||
if (devices_broker.claim(architecture.systemCallArg(state, 0), scheduler.current().id))
|
||||
architecture.setSystemCallResult(state, 0)
|
||||
else
|
||||
fail(state);
|
||||
if (devices_broker.claim(device_id, scheduler.current().id)) {
|
||||
// A display service just took the framebuffer — quiesce the bootstrap console
|
||||
// so the kernel and the service don't scribble over each other's pixels. The
|
||||
// claim releases (and the console resumes) automatically if the service dies;
|
||||
// see releaseTaskResourcesLocked.
|
||||
if (devices_broker.displayDevice()) |display_id| {
|
||||
if (device_id == display_id) console.setSuppressed(true);
|
||||
}
|
||||
architecture.setSystemCallResult(state, 0);
|
||||
} else fail(state);
|
||||
}
|
||||
|
||||
/// mmio_map(device_id, resource_index) -> vaddr: map a claimed device's MMIO window into
|
||||
@@ -341,7 +349,10 @@ fn systemMmioMap(state: *architecture.CpuState) void {
|
||||
const base_v = t.device_map_next;
|
||||
if (base_v + pages * page_size > device_arena_end) return fail(state);
|
||||
|
||||
architecture.mapUserDeviceInto(t.aspace, base_v, r.start, r.len);
|
||||
// A framebuffer resource asks (via its flag) to be mapped write-combining rather
|
||||
// than the strong-uncacheable default that register MMIO needs.
|
||||
const write_combining = (r.flags & device_abi.resource_flag_write_combining) != 0;
|
||||
architecture.mapUserDeviceInto(t.aspace, base_v, r.start, r.len, write_combining);
|
||||
t.device_map_next = base_v + pages * page_size;
|
||||
architecture.setSystemCallResult(state, base_v + (r.start & (page_size - 1))); // register base
|
||||
}
|
||||
@@ -601,6 +612,9 @@ fn releaseTaskResourcesLocked(t: *scheduler.Task) void {
|
||||
recordExitLocked(t);
|
||||
irq.releaseOwner(t.id);
|
||||
devices_broker.releaseAllOwnedBy(t.id);
|
||||
// If that dropped the framebuffer claim (this task was the display service), let the
|
||||
// bootstrap console draw again — the screen is nobody's now, so panics/status land.
|
||||
if (!devices_broker.displayClaimed()) console.setSuppressed(false);
|
||||
// The dying task's signal endpoint and one-shot timers go with it.
|
||||
if (t.signal_endpoint) |raw| {
|
||||
ipc.dropRef(@ptrCast(@alignCast(raw)));
|
||||
|
||||
+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