gfx: draw the console in RAM, and write it to the card write-combined
On the first real machine with a discrete card (an RTX 3060, 800x600 from the UEFI GOP) the console was slow enough to photograph mid-scroll, and the boot died on CPU 1's "cpu1: starting": CPU 0 spun 200M times on the console lock while CPU 1 held it, scrolling, and called it a deadlock. The framebuffer is device memory in the uncached 3-4 GB page of the direct map. QEMU's stdvga is RAM underneath, so nothing showed there; on a card, gfx_scroll_up's memmove read the whole screen back across the bus one byte at a time, and every anti-aliased glyph read back each pixel it blended. So the console now draws into a copy of the screen in RAM (vmap_alloc: kalloc pages behind a page table of their own, since nothing needs it contiguous), and gfx_flush() copies the changed rectangle onto the card through an ioremap_wc() mapping -- PAT entry 4 set to write-combining on every CPU. Flushes happen where drawing ends: a message, a terminal write, an echo, a terminal switch. Scrolling moves eight bytes at a time instead of one. /dev/fb0 reads and writes the card itself, as before. The early console's fb_info now carries the physical address it always should have. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
parent
93f981c7ea
commit
60d06658f0
9 changed files with 347 additions and 8 deletions
|
|
@ -7,6 +7,7 @@
|
|||
#include "mmu.h"
|
||||
#include "proc.h"
|
||||
#include "ioperm.h"
|
||||
#include "vmap.h"
|
||||
|
||||
int mappages(pml4e_t *pgdir, void *va, addr_t size, addr_t pa, int perm);
|
||||
static pte_t * walkpgdir(pml4e_t *pml4, const void *va, int alloc);
|
||||
|
|
@ -336,6 +337,100 @@ ioremap(uint64 pa, uint64 size)
|
|||
return (void *)(KERNBASE + ((uint64)first << 30) + (pa - base));
|
||||
}
|
||||
|
||||
// --- write-combining, and big buffers -----------------------------------------
|
||||
//
|
||||
// See include/vmap.h. The console used to draw straight into the framebuffer
|
||||
// through the direct map, which is uncached across the whole PCI hole: every
|
||||
// store was its own bus transaction and every read -- a scroll is a memmove,
|
||||
// so a read for every byte on screen -- waited on the card. In QEMU that is
|
||||
// free. On a real graphics card a single scroll took seconds, long enough
|
||||
// that a second CPU waiting to print gave up and called it a deadlock.
|
||||
|
||||
#define MSR_PAT 0x277
|
||||
#define PAT_WC 0x01
|
||||
#define PDE_PAT (1ULL << 12) // in a 2 MB entry; PAT index 4 with PCD=PWT=0
|
||||
#define PGSIZE_2M (2ULL << 20)
|
||||
|
||||
void
|
||||
pat_init(void)
|
||||
{
|
||||
// Entry 4 is write-back at reset and nothing here sets the PAT bit, so
|
||||
// repurposing it changes no existing mapping. Entries 0-3, which every
|
||||
// other mapping selects through PCD and PWT, keep their reset values.
|
||||
uint64 pat = rdmsr(MSR_PAT);
|
||||
pat = (pat & ~(0xFFULL << 32)) | ((uint64)PAT_WC << 32);
|
||||
wrmsr(MSR_PAT, pat);
|
||||
}
|
||||
|
||||
// A slot of the device window pointing at a fresh page directory.
|
||||
static pde_t *
|
||||
window_pd(int *slot)
|
||||
{
|
||||
pde_t *pd;
|
||||
|
||||
if (ioremap_next >= IOREMAP_LAST_PDPT)
|
||||
return 0;
|
||||
if ((pd = (pde_t *)kalloc()) == 0)
|
||||
return 0;
|
||||
memset(pd, 0, PGSIZE);
|
||||
*slot = ioremap_next++;
|
||||
kpdpt[*slot] = v2p(pd) | PTE_P | PTE_W;
|
||||
return pd;
|
||||
}
|
||||
|
||||
void *
|
||||
ioremap_wc(uint64 pa, uint64 size)
|
||||
{
|
||||
uint64 base = pa & ~(uint64)(PGSIZE_2M - 1);
|
||||
uint64 n = (pa + size - base + PGSIZE_2M - 1) / PGSIZE_2M;
|
||||
pde_t *pd;
|
||||
int slot;
|
||||
|
||||
if (size == 0 || n > 512 || (pd = window_pd(&slot)) == 0)
|
||||
return 0;
|
||||
// An uncached alias of the same frames is the direct map's 3-4 GB page.
|
||||
// Two uncached types for one device page is allowed; what the manuals
|
||||
// warn against is a cached alias, and there is none.
|
||||
for (uint64 i = 0; i < n; i++)
|
||||
pd[i] = (base + i * PGSIZE_2M) | PTE_P | PTE_W | PTE_PS | PDE_PAT;
|
||||
switchkvm();
|
||||
return (void *)(KERNBASE + ((uint64)slot << 30) + (pa - base));
|
||||
}
|
||||
|
||||
void *
|
||||
vmap_alloc(uint64 size)
|
||||
{
|
||||
uint64 pages = (size + PGSIZE - 1) / PGSIZE;
|
||||
pde_t *pd;
|
||||
int slot;
|
||||
|
||||
if (size == 0 || pages > 512 * 512 || (pd = window_pd(&slot)) == 0)
|
||||
return 0;
|
||||
for (uint64 i = 0; i < pages; i++) {
|
||||
pte_t *pt;
|
||||
char *page;
|
||||
if (pd[i / 512] == 0) {
|
||||
if ((pt = (pte_t *)kalloc()) == 0)
|
||||
goto oom;
|
||||
memset(pt, 0, PGSIZE);
|
||||
pd[i / 512] = v2p(pt) | PTE_P | PTE_W;
|
||||
}
|
||||
pt = (pte_t *)P2V(PTE_ADDR(pd[i / 512]));
|
||||
if ((page = kalloc()) == 0)
|
||||
goto oom;
|
||||
memset(page, 0, PGSIZE);
|
||||
pt[i % 512] = v2p(page) | PTE_P | PTE_W;
|
||||
}
|
||||
switchkvm();
|
||||
return (void *)(KERNBASE + ((uint64)slot << 30));
|
||||
|
||||
oom:
|
||||
// Whatever was mapped stays mapped and is lost. This runs once at boot,
|
||||
// and a machine out of memory by then has bigger problems.
|
||||
cprintf("vmap: out of memory for %d KB\n", (int)(size / 1024));
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
switchuvm(struct proc *p)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue