paging: map the framebuffer write-combining, and clear it after paging
The console's one-time full-screen clear was millions of individual uncached word-writes to the GPU BAR: fine in QEMU, but on real hardware firmware MTRRs force that region uncacheable, so the clear crawls. Program PAT entry 4 to write-combining (setupPat, on the BSP and every AP) and map the framebuffer window with the PAT bit, so those writes batch into bursts. The clear ran on the *loader's* uncached mapping because console.init happened before enablePaging. Move it to just after paging, so it uses the kernel's write-combining mapping instead. The cost: on-screen output is absent before paging (an early panic still lands in the serial/RAM log). Verified: 11 QEMU cases (incl. SMP AP-PAT, USB/FAT/ACPI) plus a screendump showing the framebuffer cleared to black with the status text rendered correctly.
This commit is contained in:
@@ -32,6 +32,13 @@ const device_grant: u64 = 1 << 9; // available bit: this leaf maps device MMIO,
|
|||||||
const no_execute: u64 = 1 << 63;
|
const no_execute: u64 = 1 << 63;
|
||||||
const address_mask: u64 = 0x000F_FFFF_FFFF_F000;
|
const address_mask: u64 = 0x000F_FFFF_FFFF_F000;
|
||||||
|
|
||||||
|
// The PAT-index bit. In a 4 KiB PTE it is bit 7; in a huge leaf (2 MiB PDE / 1 GiB
|
||||||
|
// PDPTE) bit 7 is PS, so the PAT bit moves to bit 12. With PCD=PWT=0 this selects
|
||||||
|
// PAT entry 4, which `setupPat` programs to write-combining (see mapRangePhysmap).
|
||||||
|
const pte_pat: u64 = 1 << 7;
|
||||||
|
const huge_pat: u64 = 1 << 12;
|
||||||
|
const ia32_pat: u32 = 0x277;
|
||||||
|
|
||||||
/// The physmap's page size for 2 MiB-aligned RAM: one PD leaf covers this instead
|
/// The physmap's page size for 2 MiB-aligned RAM: one PD leaf covers this instead
|
||||||
/// of 512 PT entries. 4 KiB pages fill the unaligned edges (see mapRangePhysmap).
|
/// of 512 PT entries. 4 KiB pages fill the unaligned edges (see mapRangePhysmap).
|
||||||
const huge_page_size: u64 = 2 << 20; // 2 MiB
|
const huge_page_size: u64 = 2 << 20; // 2 MiB
|
||||||
@@ -131,18 +138,22 @@ fn mapHugePage(pml4: u64, virtual: u64, physical: u64, flags: u64) void {
|
|||||||
/// `flags`, rounded out to whole pages. This is how the kernel keeps a permanent
|
/// `flags`, rounded out to whole pages. This is how the kernel keeps a permanent
|
||||||
/// window onto physical memory once the low identity map goes away. The 2 MiB-
|
/// window onto physical memory once the low identity map goes away. The 2 MiB-
|
||||||
/// aligned interior is mapped with huge pages; the unaligned head/tail with 4 KiB.
|
/// aligned interior is mapped with huge pages; the unaligned head/tail with 4 KiB.
|
||||||
fn mapRangePhysmap(pml4: u64, physical_base: u64, len: u64, flags: u64) void {
|
/// `write_combining` selects the WC memory type (setupPat's PAT entry 4) via the
|
||||||
|
/// PAT bit — bit 7 in a 4 KiB PTE, bit 12 in a huge leaf — for the framebuffer.
|
||||||
|
fn mapRangePhysmap(pml4: u64, physical_base: u64, len: u64, flags: u64, write_combining: bool) void {
|
||||||
|
const pte_flags = if (write_combining) flags | pte_pat else flags;
|
||||||
|
const huge_flags = if (write_combining) flags | huge_pat else flags;
|
||||||
var address = physical_base & ~@as(u64, page_size - 1);
|
var address = physical_base & ~@as(u64, page_size - 1);
|
||||||
const end = physical_base + len;
|
const end = physical_base + len;
|
||||||
// Head: 4 KiB pages up to the next 2 MiB boundary.
|
// Head: 4 KiB pages up to the next 2 MiB boundary.
|
||||||
while (address < end and address & (huge_page_size - 1) != 0) : (address += page_size)
|
while (address < end and address & (huge_page_size - 1) != 0) : (address += page_size)
|
||||||
mapPage(pml4, boot_handoff.physicalToVirtual(address), address, flags);
|
mapPage(pml4, boot_handoff.physicalToVirtual(address), address, pte_flags);
|
||||||
// Interior: 2 MiB huge pages while a whole one still fits.
|
// Interior: 2 MiB huge pages while a whole one still fits.
|
||||||
while (address + huge_page_size <= end) : (address += huge_page_size)
|
while (address + huge_page_size <= end) : (address += huge_page_size)
|
||||||
mapHugePage(pml4, boot_handoff.physicalToVirtual(address), address, flags);
|
mapHugePage(pml4, boot_handoff.physicalToVirtual(address), address, huge_flags);
|
||||||
// Tail: 4 KiB pages for whatever is left.
|
// Tail: 4 KiB pages for whatever is left.
|
||||||
while (address < end) : (address += page_size)
|
while (address < end) : (address += page_size)
|
||||||
mapPage(pml4, boot_handoff.physicalToVirtual(address), address, flags);
|
mapPage(pml4, boot_handoff.physicalToVirtual(address), address, pte_flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn regions(mm: boot_handoff.MemoryMap) []const boot_handoff.MemoryRegion {
|
fn regions(mm: boot_handoff.MemoryMap) []const boot_handoff.MemoryRegion {
|
||||||
@@ -156,11 +167,26 @@ fn enableNx() void {
|
|||||||
io.wrmsr(efer_msr, io.rdmsr(efer_msr) | (1 << 11));
|
io.wrmsr(efer_msr, io.rdmsr(efer_msr) | (1 << 11));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Program this core's PAT so entry 4 (selected by the PAT bit with PCD=PWT=0) is
|
||||||
|
/// **write-combining**, leaving the other seven at their reset types. Nothing else
|
||||||
|
/// in danos sets the PAT bit, so this changes no existing mapping — it only gives
|
||||||
|
/// the framebuffer a write-combining type, which turns its full-screen clear from
|
||||||
|
/// glacial (uncached writes to a GPU BAR, the real-hardware default via MTRRs) into
|
||||||
|
/// a batched burst. Must run on **every** core (PAT is per-logical-processor) — the
|
||||||
|
/// framebuffer mapping lives in the shared kernel half, so a core with the reset
|
||||||
|
/// PAT would see it as write-back and alias. Called from `init` (BSP) and each AP.
|
||||||
|
pub fn setupPat() void {
|
||||||
|
// Reset PAT is PA0=WB PA1=WT PA2=UC- PA3=UC PA4=WB PA5=WT PA6=UC- PA7=UC; flip
|
||||||
|
// PA4 from WB (0x06) to WC (0x01). Type codes: UC=0 WC=1 WT=4 WP=5 WB=6 UC-=7.
|
||||||
|
io.wrmsr(ia32_pat, 0x0007_0401_0007_0406);
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the address space and switch onto it.
|
/// Build the address space and switch onto it.
|
||||||
pub fn init(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot_information: *const boot_handoff.BootInformation) void {
|
pub fn init(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot_information: *const boot_handoff.BootInformation) void {
|
||||||
alloc_frame = allocFrame;
|
alloc_frame = allocFrame;
|
||||||
free_frame = freeFrame;
|
free_frame = freeFrame;
|
||||||
enableNx();
|
enableNx();
|
||||||
|
setupPat(); // BSP: PAT entry 4 = write-combining, for the framebuffer window
|
||||||
const pml4 = allocTable();
|
const pml4 = allocTable();
|
||||||
|
|
||||||
// 1. All RAM in the physmap (physicalToVirtual(physical)) RW + NX. No identity/low-half
|
// 1. All RAM in the physmap (physicalToVirtual(physical)) RW + NX. No identity/low-half
|
||||||
@@ -168,13 +194,15 @@ pub fn init(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot
|
|||||||
// mapped on demand (mapMmio) or explicitly below.
|
// mapped on demand (mapMmio) or explicitly below.
|
||||||
for (regions(boot_information.memory_map)) |r| {
|
for (regions(boot_information.memory_map)) |r| {
|
||||||
if (r.kind == .mmio) continue;
|
if (r.kind == .mmio) continue;
|
||||||
mapRangePhysmap(pml4, r.base, r.pages * page_size, present | writable | no_execute);
|
mapRangePhysmap(pml4, r.base, r.pages * page_size, present | writable | no_execute, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Physmap windows for the framebuffer and the Local APIC (device memory
|
// 2. Physmap windows for the framebuffer and the Local APIC (device memory
|
||||||
// the kernel touches directly), RW + NX.
|
// the kernel touches directly), RW + NX. The framebuffer is **write-
|
||||||
|
// combining** (see setupPat) so the console's full-screen clear is a burst,
|
||||||
|
// not millions of uncached single-word writes.
|
||||||
const fb = boot_information.framebuffer;
|
const fb = boot_information.framebuffer;
|
||||||
mapRangePhysmap(pml4, fb.base, @as(u64, fb.height) * fb.pitch, present | writable | no_execute);
|
mapRangePhysmap(pml4, fb.base, @as(u64, fb.height) * fb.pitch, present | writable | no_execute, true);
|
||||||
mapPage(pml4, boot_handoff.physicalToVirtual(0xFEE00000), 0xFEE00000, present | writable | no_execute);
|
mapPage(pml4, boot_handoff.physicalToVirtual(0xFEE00000), 0xFEE00000, present | writable | no_execute);
|
||||||
|
|
||||||
// 3. The kernel's own segments at their higher-half link addresses, mapped
|
// 3. The kernel's own segments at their higher-half link addresses, mapped
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ fn delayMicros(us: u64) void {
|
|||||||
/// signals the BSP, then jumps to the generic scheduler entry. Never returns.
|
/// signals the BSP, then jumps to the generic scheduler entry. Never returns.
|
||||||
fn apEntry(percpu: usize) callconv(.c) noreturn {
|
fn apEntry(percpu: usize) callconv(.c) noreturn {
|
||||||
const cpu = boot_index;
|
const cpu = boot_index;
|
||||||
|
paging.setupPat(); // this core's PAT: entry 4 = write-combining, to match the BSP
|
||||||
gdt.loadOnThisCpu(cpu); // this core's GDT (with its own TSS slot)
|
gdt.loadOnThisCpu(cpu); // this core's GDT (with its own TSS slot)
|
||||||
tss.setupThisCpu(cpu); // this core's TSS + IST stack, loaded into TR
|
tss.setupThisCpu(cpu); // this core's TSS + IST stack, loaded into TR
|
||||||
idt.loadOnThisCpu(); // the shared IDT
|
idt.loadOnThisCpu(); // the shared IDT
|
||||||
|
|||||||
@@ -80,8 +80,14 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
// surface — a bootstrap text console today, a graphics device driver later — so
|
// surface — a bootstrap text console today, a graphics device driver later — so
|
||||||
// we never assume the OS is text-based. Only a few user-facing status lines
|
// we never assume the OS is text-based. Only a few user-facing status lines
|
||||||
// (via `status`) and panics are mirrored to it; the verbose log stays out.
|
// (via `status`) and panics are mirrored to it; the verbose log stays out.
|
||||||
|
//
|
||||||
|
// The console is brought up *after* paging (below), not here: its one-time
|
||||||
|
// full-screen clear then runs on the kernel's **write-combining** mapping of the
|
||||||
|
// framebuffer instead of the loader's uncached one — a fast burst rather than
|
||||||
|
// millions of uncached writes on real hardware. Until then, on-screen output is
|
||||||
|
// absent (an early panic still lands in the serial/RAM log); the trade is worth
|
||||||
|
// a near-instant boot. `console.write` is a safe no-op while the console is down.
|
||||||
const fb = boot_information.framebuffer;
|
const fb = boot_information.framebuffer;
|
||||||
console.init(fb);
|
|
||||||
|
|
||||||
log.checkpoint(cp_entry);
|
log.checkpoint(cp_entry);
|
||||||
|
|
||||||
@@ -91,10 +97,6 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
architecture.init();
|
architecture.init();
|
||||||
|
|
||||||
status("/system/kernel: initialising kernel...\n");
|
status("/system/kernel: initialising kernel...\n");
|
||||||
log.write(if (console.present())
|
|
||||||
"/system/kernel: framebuffer console online (bootstrap; graphics driver later)\n"
|
|
||||||
else
|
|
||||||
"/system/kernel: no framebuffer (headless) -> logging to serial/debugcon only\n");
|
|
||||||
if (build_options.serial) log.write(if (architecture.serialPresent())
|
if (build_options.serial) log.write(if (architecture.serialPresent())
|
||||||
"/system/kernel: serial console online (COM1)\n"
|
"/system/kernel: serial console online (COM1)\n"
|
||||||
else
|
else
|
||||||
@@ -154,6 +156,15 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||||||
log.print(" page tables: root = 0x{x:0>16}\n", .{architecture.activePageTable()});
|
log.print(" page tables: root = 0x{x:0>16}\n", .{architecture.activePageTable()});
|
||||||
log.print(" kernel segs: {d} (mapped with W^X permissions)\n", .{boot_information.kernel_segment_count});
|
log.print(" kernel segs: {d} (mapped with W^X permissions)\n", .{boot_information.kernel_segment_count});
|
||||||
|
|
||||||
|
// Now on our own tables, the framebuffer window is write-combining: bring up
|
||||||
|
// the on-screen console and clear it (a fast burst here, not the loader's
|
||||||
|
// uncached crawl). From here `status` reaches the screen as well as the log.
|
||||||
|
console.init(fb);
|
||||||
|
log.write(if (console.present())
|
||||||
|
"/system/kernel: framebuffer console online (bootstrap; graphics driver later)\n"
|
||||||
|
else
|
||||||
|
"/system/kernel: no framebuffer (headless) -> logging to serial/debugcon only\n");
|
||||||
|
|
||||||
// Bring up the kernel heap (dynamic allocation), built on the VMM.
|
// Bring up the kernel heap (dynamic allocation), built on the VMM.
|
||||||
heap.init();
|
heap.init();
|
||||||
log.checkpoint(cp_heap);
|
log.checkpoint(cp_heap);
|
||||||
|
|||||||
Reference in New Issue
Block a user