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:
parent
c4595700ba
commit
10c11d1806
|
|
@ -32,6 +32,13 @@ const device_grant: u64 = 1 << 9; // available bit: this leaf maps device MMIO,
|
|||
const no_execute: u64 = 1 << 63;
|
||||
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
|
||||
/// of 512 PT entries. 4 KiB pages fill the unaligned edges (see mapRangePhysmap).
|
||||
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
|
||||
/// 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.
|
||||
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);
|
||||
const end = physical_base + len;
|
||||
// Head: 4 KiB pages up to the next 2 MiB boundary.
|
||||
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.
|
||||
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.
|
||||
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 {
|
||||
|
|
@ -156,11 +167,26 @@ fn enableNx() void {
|
|||
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.
|
||||
pub fn init(allocFrame: *const fn () ?u64, freeFrame: *const fn (u64) void, boot_information: *const boot_handoff.BootInformation) void {
|
||||
alloc_frame = allocFrame;
|
||||
free_frame = freeFrame;
|
||||
enableNx();
|
||||
setupPat(); // BSP: PAT entry 4 = write-combining, for the framebuffer window
|
||||
const pml4 = allocTable();
|
||||
|
||||
// 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.
|
||||
for (regions(boot_information.memory_map)) |r| {
|
||||
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
|
||||
// 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;
|
||||
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);
|
||||
|
||||
// 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.
|
||||
fn apEntry(percpu: usize) callconv(.c) noreturn {
|
||||
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)
|
||||
tss.setupThisCpu(cpu); // this core's TSS + IST stack, loaded into TR
|
||||
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
|
||||
// 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.
|
||||
//
|
||||
// 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;
|
||||
console.init(fb);
|
||||
|
||||
log.checkpoint(cp_entry);
|
||||
|
||||
|
|
@ -91,10 +97,6 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
|||
architecture.init();
|
||||
|
||||
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())
|
||||
"/system/kernel: serial console online (COM1)\n"
|
||||
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(" 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.
|
||||
heap.init();
|
||||
log.checkpoint(cp_heap);
|
||||
|
|
|
|||
Loading…
Reference in New Issue