From 182cc765887cebf14e7eaea33181c9ea0262ad1a Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Tue, 22 Sep 2026 21:47:35 +0200 Subject: [PATCH] gfx: the second head shows the boot, not just what comes after it Two things, both asked for. **The crash report no longer says which monitors it reached.** "The report is on the virtio-gpu display too" was a line about the crash screen printed on the crash screen. The report says what happened and where to read it again; that is not part of it. **And the second head now shows the F8 menu and the boot.** It used to be black until vgpud started several seconds into userspace -- through the splash, through the menu, through every boot message -- which from the outside is indistinguishable from a display that does not work. Three stages now cover the whole boot and hand over to each other without coordinating: *The loader.* LocateProtocol() answers with the first GOP and offers no way to ask for the rest, so the rest are found by handle and fed the primary's framebuffer as a Blt source at the points where a screen is finished: the menu, the resolution chooser, system information, the splash, each segment of the kernel load. Blt rather than a second framebuffer to draw into, because a virtio GOP does not have one -- the same fact that made a virtio-only machine reboot-loop two commits ago. *The kernel.* gfx remembers which rows were drawn, as one span rather than a list of rectangles, because console output is lines and a span is what lines make. display_mirror_console() copies that span across: a line of boot text is one cell-row transfer, not a screen. Primed once after pci_init(), because a display that has just appeared has a black scanout and the console it should be showing was drawn by the loader into a framebuffer it knows nothing about. *Then vgpud*, from the same console framebuffer, unchanged. **Two bugs found building it, both worth the retelling.** The mirror ran per character from inside vt_feed -- which is inside the console lock -- and vga_disp_acquire() announces its back buffer with a cprintf. A CPU taking a lock it already holds, with a call chain that said exactly that. It runs at the end of consolewrite() and cprintf() now, outside the lock, which is also the better batching: one transfer per write instead of one per character. And "is this the console's own screen" was asked of the pointer acquire() returned. vga_fb hands back a back buffer it copies onto the console's framebuffer afterwards, so on a one-screen machine the answer was "a different screen" -- and the console was mirrored onto itself through two megabytes of intermediate, allocated for the purpose. display_ops grew scanout_phys, which is a fact about the hardware rather than a policy a driver can get subtly wrong, and it is asked first: it has no side effects and acquire() has two. Verified with two heads under QEMU, screendumping the virtio head every 400 ms from power-on: splash, splash, splash, then the console, and it stays. The F8 menu appears on it; choosing 1024x768 there brings both heads up at 1024x768 byte-identical. A panic still reaches both. Both supported configurations: one boot each, no traps, no panics, socktest and memfdtest green. One gap remains and is documented rather than hidden: while the kernel's driver resets the device during probe, QEMU shows "Display output is not active" for a fraction of a second. A device being taken over has no scanout until the new driver sets one. Co-Authored-By: Claude Opus 5 --- NOTICE | 21 --- boot/uefi/efi.h | 25 +++- boot/uefi/main.c | 93 ++++++++++++ docs/architecture/graphics.md | 55 +++++++- include/defs.h | 4 + include/display.h | 15 ++ include/gfx.h | 4 + kernel/core/console.c | 20 +++ kernel/core/main.c | 12 ++ kernel/dev/device.c | 156 +++++++++++++++++++-- kernel/dev/gfx.c | 40 ++++++ kernel/dev/vt.c | 10 +- kernel/drivers/gpu/vga/vga_fb.c | 11 ++ kernel/drivers/gpu/virtio_gpu/virtio_gpu.c | 13 ++ 14 files changed, 437 insertions(+), 42 deletions(-) diff --git a/NOTICE b/NOTICE index d029b17..0192f92 100644 --- a/NOTICE +++ b/NOTICE @@ -183,27 +183,6 @@ not be sold on its own. Bundling it with an operating system is expressly permitted. JetBrains Mono declares no Reserved Font Name, so a modified version (a pre-rasterised bitmap, for instance) may keep the name. --------------------------------------------------------------------------------- -"New File System For XV6" MIT License --------------------------------------------------------------------------------- -Copyright (c) Qi Ma, Leyi Pan, Ao Sun, Peiran Xu - -kernel/fs/fs.c and parts of tools/mkfs.c derive from this project, which extends -xv6's file system with features SchOS still uses today: - - - the HTree hashed directory index (BKDRHash, HASHRANGE, the primary/overflow - bucket layout), after Daniel Phillips, "A Directory Index for EXT2", - 5th Annual Linux Showcase & Conference, 2001 - - Ext4-style extents (T_EXTENT) - - the inode bitmap - - symbolic links, file permissions, lseek and append - -Licensed under the MIT License, which permits this use and requires that the -copyright notice and permission notice be retained. This entry is that -retention; it was missing until 2026-08-09 and is recorded here regardless of -what happens to the code, because the obligation attaches to every copy already -made. - Distinct from xv6 itself (LICENSE.MIT), which this project in turn derives from. ================================================================================ diff --git a/boot/uefi/efi.h b/boot/uefi/efi.h index 908c453..41a8e62 100644 --- a/boot/uefi/efi.h +++ b/boot/uefi/efi.h @@ -171,7 +171,12 @@ typedef struct { void *OpenProtocolInformation; void *ProtocolsPerHandle; - void *LocateHandleBuffer; + /* SearchType 2 is ByProtocol: every handle that speaks a given protocol, + * which is how the loader finds the displays LocateProtocol does not + * return -- it answers with the first one and there is no second call. */ + EFI_STATUS (*LocateHandleBuffer)(UINTN SearchType, EFI_GUID *Protocol, + void *SearchKey, UINTN *NoHandles, + EFI_HANDLE **Buffer); EFI_STATUS (*LocateProtocol)(EFI_GUID *Protocol, void *Registration, void **Interface); } EFI_BOOT_SERVICES; @@ -307,10 +312,26 @@ typedef EFI_STATUS (*EFI_GOP_QUERY_MODE)( typedef EFI_STATUS (*EFI_GOP_SET_MODE)( struct EFI_GRAPHICS_OUTPUT_PROTOCOL *This, UINT32 ModeNumber); +/* Blt is how you draw on a GOP that has no linear framebuffer to write into -- + * and there is one of those on every machine with a virtio-gpu, because the + * device has no aperture. It is also the only way to put pixels on a *second* + * display: the loader draws into the primary's framebuffer directly and then + * hands that same memory to the other GOP's Blt as a source buffer. */ +struct EFI_GRAPHICS_OUTPUT_PROTOCOL; +#define ByProtocol 2 +#define EfiBltVideoFill 0 +#define EfiBltBufferToVideo 2 + +typedef EFI_STATUS (*EFI_GOP_BLT)(struct EFI_GRAPHICS_OUTPUT_PROTOCOL *This, + void *BltBuffer, UINTN BltOperation, + UINTN SourceX, UINTN SourceY, + UINTN DestinationX, UINTN DestinationY, + UINTN Width, UINTN Height, UINTN Delta); + typedef struct EFI_GRAPHICS_OUTPUT_PROTOCOL { EFI_GOP_QUERY_MODE QueryMode; EFI_GOP_SET_MODE SetMode; - void *Blt; + EFI_GOP_BLT Blt; EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE *Mode; } EFI_GRAPHICS_OUTPUT_PROTOCOL; diff --git a/boot/uefi/main.c b/boot/uefi/main.c index e6daa22..28c931e 100644 --- a/boot/uefi/main.c +++ b/boot/uefi/main.c @@ -79,6 +79,8 @@ static EFI_BOOT_SERVICES *BS; // Console. Enough to say what went wrong, and no more. // --------------------------------------------------------------------------- +static void mirrors_show(void); /* see "the other displays", further down */ + /* Once the splash is up, nothing may write to the console. * * ConOut on a graphical firmware renders text into the same framebuffer the @@ -412,6 +414,7 @@ load_elf(EFI_FILE_PROTOCOL *file) done += ph.p_memsz; ui_progress_tick((ui_u32)done); + mirrors_show(); /* once per segment: a handful of times, not a loop */ if (ph.p_paddr < lowest) lowest = ph.p_paddr; if (ph.p_paddr + ph.p_memsz > highest) highest = ph.p_paddr + ph.p_memsz; @@ -606,6 +609,7 @@ res_read_number(const char *prompt) const char *rows[1] = { shown }; ui_menu("Custom resolution", rows, 1, 0, "digits, Enter to accept"); + mirrors_show(); EFI_INPUT_KEY key; for (;;) { @@ -623,6 +627,8 @@ res_read_number(const char *prompt) } } + + static void res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) { @@ -654,6 +660,7 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) serial_item(items, sel, sel, 0); } echoed = sel; + mirrors_show(); EFI_INPUT_KEY key; for (;;) { @@ -702,6 +709,7 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) * and letting the user wonder whether the key registered. */ const char *msg[1] = { "No such mode -- keeping this one" }; ui_menu("Resolution", msg, 1, 0, "any key to go back"); + mirrors_show(); for (;;) { if (ST->ConIn && !EFI_ERROR(ST->ConIn->ReadKeyStroke(ST->ConIn, &key))) @@ -905,6 +913,7 @@ sys_info(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) serial_puts("\n"); } serial_puts("Any key to go back\n"); + mirrors_show(); EFI_INPUT_KEY key; for (;;) { @@ -955,6 +964,7 @@ boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) serial_item(items, selected, selected, 1); } echoed = selected; + mirrors_show(); EFI_INPUT_KEY key; CHAR16 c = 0; @@ -988,6 +998,83 @@ boot_menu(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop) } } +/* --- the other displays ----------------------------------------------------- + * + * A machine with two adapters has two screens, and the loader drew on one of + * them. The other was black through the splash, black through the F8 menu, and + * black until vgpud started several seconds into userspace -- which from the + * outside is indistinguishable from a display that does not work. + * + * LocateProtocol() answers with the first GOP and offers no way to ask for the + * rest, so the rest are found by handle. Each one is then fed the *primary's + * framebuffer* as a Blt source: the loader keeps drawing into one piece of + * memory exactly as before, and this copies it outward at the points where a + * screen is finished. + * + * Blt rather than a second framebuffer to draw into, because a virtio GOP does + * not have one -- it reports a FrameBufferBase of zero and implements Blt and + * nothing else. Which is the same fact that made a virtio-only machine + * reboot-loop; see kernel/core/main.c. + */ +#define MAX_MIRRORS 4 +static EFI_GRAPHICS_OUTPUT_PROTOCOL *mirror_gop[MAX_MIRRORS]; +static EFI_GRAPHICS_OUTPUT_PROTOCOL *primary_gop; +static UINTN mirror_n; + +static void +mirrors_find(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary) +{ + EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID; + EFI_HANDLE *handles = 0; + UINTN count = 0; + + primary_gop = primary; + + if (!BS->LocateHandleBuffer || + EFI_ERROR(BS->LocateHandleBuffer(ByProtocol, &gop_guid, 0, + &count, &handles)) || !handles) + return; + + for (UINTN i = 0; i < count && mirror_n < MAX_MIRRORS; i++) { + EFI_GRAPHICS_OUTPUT_PROTOCOL *g = 0; + if (EFI_ERROR(BS->HandleProtocol(handles[i], &gop_guid, (void **)&g))) + continue; + if (!g || g == primary || !g->Blt || !g->Mode || !g->Mode->Info) + continue; + mirror_gop[mirror_n++] = g; + } +} + +/* Put what is on the primary onto all of them. Centred when the sizes differ, + * cropped when the other display is smaller -- the same rules the kernel's + * console mirror and vgpud's both use, because it is the same picture. */ +static void +mirrors_show(void) +{ + EFI_GRAPHICS_OUTPUT_PROTOCOL *primary = primary_gop; + + if (!mirror_n || !primary || !primary->Mode || !primary->Mode->Info) + return; + if (!primary->Mode->FrameBufferBase) + return; /* nothing to copy *from* */ + + UINTN sw = primary->Mode->Info->HorizontalResolution; + UINTN sh = primary->Mode->Info->VerticalResolution; + UINTN delta = (UINTN)primary->Mode->Info->PixelsPerScanLine * 4; + + for (UINTN i = 0; i < mirror_n; i++) { + EFI_GRAPHICS_OUTPUT_PROTOCOL *g = mirror_gop[i]; + UINTN w = g->Mode->Info->HorizontalResolution; + UINTN h = g->Mode->Info->VerticalResolution; + UINTN cw = w < sw ? w : sw; + UINTN ch = h < sh ? h : sh; + + g->Blt(g, (void *)(UINTN)primary->Mode->FrameBufferBase, + EfiBltBufferToVideo, 0, 0, (w - cw) / 2, (h - ch) / 2, + cw, ch, delta); + } +} + /* Wait briefly for F8, exactly as the BIOS loader does, and show the same * menu if it comes. One prompt, one key, one screen -- the resolution chooser * lives in that menu rather than in a second prompt of its own. @@ -1027,6 +1114,11 @@ setup_framebuffer(void) return 0; } + // The other displays, before anything is drawn: the F8 menu is the first + // thing on screen and there is no reason for it to be the one screen that + // does not reach them. + mirrors_find(gop); + // F8 first, on whatever mode the firmware left us in, because the menu has // to be visible in order to offer a different one. Anything it changes is // reflected below, which is why nothing is read from gop->Mode until after. @@ -1070,6 +1162,7 @@ setup_framebuffer(void) ui_init(&uifb); ui_splash(0); + mirrors_show(); // From here on, diagnostics go to COM1 rather than the console, or each one // is drawn across the splash. diff --git a/docs/architecture/graphics.md b/docs/architecture/graphics.md index 8b02daa..5607318 100644 --- a/docs/architecture/graphics.md +++ b/docs/architecture/graphics.md @@ -122,6 +122,53 @@ not work**, and that is exactly what it got called. It shows the console now. +### Before vgpud there is, the kernel and the loader do it + +`vgpud` starts several seconds into userspace. Until then the second head used +to be black — through the splash, through the F8 menu, through every boot +message — which from the outside is indistinguishable from a display that does +not work. Three stages now cover the whole boot, and they hand over to each +other without coordinating: + +**The loader.** `LocateProtocol()` answers with the first GOP and offers no way +to ask for the rest, so the rest are found by handle (`LocateHandleBuffer`, +`ByProtocol`). Each is fed the *primary's framebuffer* as a `Blt` source, at +the points where a screen is finished: the F8 menu, the resolution chooser, +system information, the splash, each segment of the kernel load. Blt rather +than a second framebuffer to draw into, because a virtio GOP does not have one +— the same fact that made a virtio-only machine reboot-loop. + +**The kernel.** `display_mirror_console()` copies the console's newly-drawn +rows to the display class whenever that is a *different screen*. Cheap because +`gfx` remembers which rows those were: a line of boot text is one cell-row +transfer, not a screen. + +Two things about that were learned the hard way, and both are load-bearing: + +- **It runs outside the console lock**, at the end of `consolewrite()` and + `cprintf()` rather than per character inside them. A display driver is + entitled to say something — `vga_disp_acquire()` announces its back buffer — + and `cprintf()` from inside that lock is a CPU taking a lock it already + holds. The panic's call chain said so plainly, which is the argument for + keeping the call chain in the report. +- **"Is this the console's own screen" is asked of the scanout**, not of the + buffer `acquire()` returns. `vga_fb` hands back a back buffer that it later + copies onto the console's framebuffer, so comparing pointers answered "a + different screen" about the only screen there is — and mirrored the console + onto itself through two megabytes of intermediate. `display_ops` grew + `scanout_phys` to answer it as a fact about the hardware. + +**Then vgpud**, from the same console framebuffer, by the mechanism below. The +handover needs no coordination in either direction: the moment `vgpud` claims +the device the kernel's `display_acquire()` returns nothing, and if `vgpud` +exits the kernel's driver takes it back and the first line of output repaints +the screen. + +There is one visible gap, and it is honest: while the kernel's driver resets +the device during probe, QEMU shows *"Display output is not active"* for a +fraction of a second. A device being taken over has no scanout until the new +driver sets one, and the same blink happens when `vgpud` claims it. + ### A copy with a diff The console framebuffer is mapped through `/dev/fb0`; the virtio backing store @@ -365,11 +412,9 @@ to run again — and rebuilding one queue from 20 KB of static memory. No allocation, no locks, no sleeping, and every spin bounded: a crash screen that hangs has taken away the serial log's last chance to be read. -Both cases say so on the serial line: - -``` - The report is on the virtio-gpu display too. -``` +Neither announces itself on the serial line. The report says what happened and +where to read it again; which monitors it reached is not part of that, and a +crash screen is not the place to narrate the crash screen. ### It is no longer the only copy diff --git a/include/defs.h b/include/defs.h index f79d32e..50742b4 100644 --- a/include/defs.h +++ b/include/defs.h @@ -613,6 +613,10 @@ void fb_mapped(void); // The crash screen, on displays other than the console's. kernel/dev/device.c. void display_panic_present(void); +// Push the console's newly-drawn rows to a second display, if the kernel +// drives one. kernel/dev/device.c. +void display_mirror_console(void); +uint64 display_scanout_phys(void); // Where a userspace display driver said it is scanning out, and which kernel // driver it took the device from. kernel/dev/pciuser.c. int pciuser_panic_surface(int idx, uint64 *paddr, int *w, int *h, diff --git a/include/display.h b/include/display.h index b695d0f..82865c1 100644 --- a/include/display.h +++ b/include/display.h @@ -37,6 +37,21 @@ struct display_ops { // cannot tell leaves this null, and present() then simply tears less than // drawing directly would. void (*wait_vblank)(struct device *); + + // The physical address this device is scanning out from. + // + // Which answers a question acquire() cannot: **is this screen the console's + // screen, or another one?** The buffer acquire() returns is not the answer, + // because a driver is free to hand back a back buffer it will later copy + // onto the console's own framebuffer -- vga_fb does exactly that, and a + // console mirror that believed the pointer copied the console onto itself + // through two megabytes of intermediate. + // + // A fact about the hardware rather than a policy, so a driver cannot get it + // subtly wrong: compare it with gfx_fb_phys(). Optional; a driver that + // leaves it null is assumed to be the console's, which is the safe way round + // -- it costs a second screen nothing is drawn on, not a wrong picture. + uint64 (*scanout_phys)(struct device *); }; // The bound display, or 0. Convenience over device_find_class(). diff --git a/include/gfx.h b/include/gfx.h index 8447588..f4c0669 100644 --- a/include/gfx.h +++ b/include/gfx.h @@ -48,6 +48,10 @@ uint32 gfx_fb_stride(void); uint32 gfx_fb_bpp(void); volatile uchar *gfx_fb_ptr(void); +// The rows drawn since this was last called, for whoever is keeping a second +// screen in step with this one. Returns 0 when nothing has changed. +int gfx_damage_take(int *y0, int *y1); + // Pixels and shapes. void gfx_draw_pixel(int x, int y, uint32 color); void gfx_clear(uint32 color); diff --git a/kernel/core/console.c b/kernel/core/console.c index 13c2949..a810575 100644 --- a/kernel/core/console.c +++ b/kernel/core/console.c @@ -170,6 +170,13 @@ cprintf(char *fmt, ...) if (locking) release(&cons.lock); va_end(ap); + + // The kernel's own output reaches the second display too, and for the same + // reason it is done here rather than in consputc(): outside the lock. On a + // quiet boot almost nothing comes through this path -- kinfo() goes to the + // log -- so this is mostly for a verbose boot, where it is the whole point. + if (locking) + display_mirror_console(); } // A line for the log, and for the screen only on a verbose boot. @@ -753,6 +760,19 @@ consolewrite(struct inode *ip, uint64 off, char *buf, int n) for(i = 0; i < n; i++) vt_out(v, buf[i] & 0xff); release(&cons.lock); + + // And onto a second display, if the kernel drives one. + // + // **After the lock, not inside it.** A display driver may have something to + // say -- vga_disp_acquire() announces its back buffer, the virtio driver + // complains when the device does not answer -- and cprintf() from inside + // this lock is a CPU acquiring a lock it already holds, which is a panic + // with a very confusing call chain. Found exactly that way. + // + // Once per write() rather than once per character, which is also the better + // batching: a line of output is one transfer instead of eighty. + display_mirror_console(); + ilock(ip); return n; diff --git a/kernel/core/main.c b/kernel/core/main.c index 6097b27..ad8accf 100644 --- a/kernel/core/main.c +++ b/kernel/core/main.c @@ -193,6 +193,18 @@ kmain(uint32 fb_addr) pciuser_init(); pci_init(); if (boot_verbose) cprintf("verbose boot: pci_init done\n"); + + // A second display has just appeared, with a black scanout of its own, and + // the console it ought to be showing was drawn before it existed -- by the + // loader, into a framebuffer this device knows nothing about. So it is + // caught up here, once, rather than waiting for the next line of output: + // on a quiet boot that is most of a second of one screen showing the splash + // and the other showing nothing. + // + // After pci_init() rather than inside device_add(), because a driver's + // attached() hook -- which is what tells the instance apart from its + // siblings -- does not run until the device has been published. + display_mirror_console(); pinit(); if (boot_verbose) cprintf("verbose boot: pinit done\n"); binit(); diff --git a/kernel/dev/device.c b/kernel/dev/device.c index b77305a..9b965c1 100644 --- a/kernel/dev/device.c +++ b/kernel/dev/device.c @@ -18,6 +18,7 @@ #include "netdev.h" #include "theme.h" #include "text.h" +#include "spinlock.h" #include "gfx.h" #include "pci.h" #include "mmu.h" @@ -181,6 +182,18 @@ display_present(int x, int y, int w, int h) ((struct display_ops *)d->ops)->present(d, x, y, w, h); } +// Where this display's pixels actually end up. 0 when the driver does not say, +// which callers must read as "assume it is the console's" -- see the comment +// on scanout_phys in include/display.h. +uint64 +display_scanout_phys(void) +{ + struct device *d = display_get(); + if (!d || !d->ops || !((struct display_ops *)d->ops)->scanout_phys) + return 0; + return ((struct display_ops *)d->ops)->scanout_phys(d); +} + void display_wait_vblank(void) { @@ -219,8 +232,14 @@ display_wait_vblank(void) // mirror uses: the overlap, centred if the surface is larger, top-left if it // is smaller. Both sides are XRGB8888; anything else is refused by whoever // registered it. -static void -panic_copy_console(volatile uchar *dst, int dw, int dh, uint32 dstride) +// Copy rows [r0, r1] of the console framebuffer into a surface, with the same +// rules vgpud's mirror uses: the overlap, centred if the surface is larger, +// top-left if it is smaller. Both sides are XRGB8888; anything else is refused +// by whoever registered it. Returns the destination row span it wrote, so the +// caller can present exactly that. +static int +copy_console_rows(volatile uchar *dst, int dw, int dh, uint32 dstride, + int r0, int r1, int *out_y, int *out_h) { const volatile uchar *src = gfx_fb_ptr(); int sw = (int)gfx_width(), sh = (int)gfx_height(); @@ -229,15 +248,130 @@ panic_copy_console(volatile uchar *dst, int dw, int dh, uint32 dstride) int ch = sh < dh ? sh : dh; int x0 = (dw - cw) / 2, y0 = (dh - ch) / 2; - if (!src || gfx_fb_bpp() != 32) - return; + if (!src || gfx_fb_bpp() != 32 || cw <= 0 || ch <= 0) + return 0; + if (r0 < 0) r0 = 0; + if (r1 > ch - 1) r1 = ch - 1; + if (r1 < r0) + return 0; - for (int y = 0; y < ch; y++) { + for (int y = r0; y <= r1; y++) { const volatile uint32 *s = (const volatile uint32 *)(src + (uint64)y * sstride); volatile uint32 *d = (volatile uint32 *)(dst + (uint64)(y0 + y) * dstride); for (int x = 0; x < cw; x++) d[x0 + x] = s[x]; } + *out_y = y0 + r0; + *out_h = r1 - r0 + 1; + return 1; +} + +static void +panic_copy_console(volatile uchar *dst, int dw, int dh, uint32 dstride) +{ + int y, h; + copy_console_rows(dst, dw, dh, dstride, 0, dh, &y, &h); +} + +// --- the console, on a second screen --------------------------------------- +// +// A machine with two displays shows the console on one of them and whatever +// the other device happened to be left holding on the other -- which during +// boot is nothing at all: a black rectangle next to the machine starting up. +// +// So the rows the console draws are copied across as they are drawn. Cheap +// because gfx remembers which rows those were: a line of boot text is one +// cell-row transfer, not a screen. +// +// This is the same job vgpud does from userspace, and the two do not overlap. +// While the kernel drives the second GPU this runs; the moment vgpud claims +// it, display_acquire() returns nothing here and vgpud's mirror takes over +// from the same console framebuffer. The handover needs no coordination +// because neither side owns anything the other wants. +void +display_mirror_console(void) +{ + // cprintf() from inside the display driver would come straight back here + // and present again, for ever. + static int inside; + // And two terminals on two CPUs would interleave into one command queue. + // The lock is held across the copy and the present, which is microseconds of + // polled virtio and the same thing the panic path does with everything else + // already stopped. + static struct spinlock lk; + static int lk_init; + // Which buffer was last brought up to date; see below. + static volatile uchar *last_buf; + struct display_mode m; + uint32 stride = 0; + volatile uchar *buf; + int r0, r1, y, h; + + if (inside || !gfx_ready()) + return; + + // Is there a second screen at all? Asked first, and asked of the *scanout*, + // because both halves of that matter. + // + // Of the scanout, because the buffer acquire() returns is not the same + // thing: vga_fb hands back a back buffer it copies onto the console's own + // framebuffer afterwards, so comparing pointers said "a different screen" + // about the only screen there is -- and then mirrored the console onto + // itself through two megabytes of intermediate. + // + // First, because this question has no side effects and acquire() has two: + // it allocates a back buffer on first call, and it announces it with a + // cprintf -- which comes straight back here. On a one-screen machine both + // were pure waste, and the cprintf was a panic. + uint64 phys = display_scanout_phys(); + if (phys == 0 || phys == gfx_fb_phys()) + return; + + if (!lk_init) { + initlock(&lk, "dispmirror"); + lk_init = 1; + } + acquire(&lk); + // Set before the first call into the driver, not after: a driver may + // cprintf, cprintf comes back here, and the second entry would block on a + // lock its own call chain is holding. + inside = 1; + buf = display_acquire(&stride); + + if (!buf || display_mode(&m) != 0 || m.bpp != 32) { + inside = 0; + release(&lk); + return; + } + + // A buffer we have not written before is owed the whole screen, not the + // damage since last time. + // + // This is what a handover looks like from here: the loader draws the splash + // straight into the framebuffer, the kernel's display driver probes some + // milliseconds later with a black scanout of its own, and gfx has no damage + // to report because gfx did not draw any of it. Without this the second head + // went black at that moment and filled in a line at a time as output + // arrived. It happens once per driver, and the same rule covers the device + // coming back from a userspace driver that exited. + int have = gfx_damage_take(&r0, &r1); + if (buf != last_buf) { + last_buf = buf; + r0 = 0; + r1 = (int)m.height - 1; + have = 1; + } + if (!have) { + inside = 0; + release(&lk); + return; + } + + if (copy_console_rows(buf, m.width, m.height, stride ? stride : m.stride, + r0, r1, &y, &h)) + display_present(0, y, m.width, h); + inside = 0; + release(&lk); } void @@ -252,15 +386,14 @@ display_panic_present(void) { struct display_mode m; uint32 stride = 0; - struct device *d = display_get(); volatile uchar *buf = display_acquire(&stride); - if (buf && display_mode(&m) == 0 && m.bpp == 32 && - (uint64)buf != (uint64)gfx_fb_ptr()) { + uint64 phys = display_scanout_phys(); + + if (buf && phys && phys != gfx_fb_phys() && + display_mode(&m) == 0 && m.bpp == 32) { panic_copy_console(buf, m.width, m.height, stride ? stride : m.stride); display_present(0, 0, m.width, m.height); - cprintf(" The report is on the %s display too.\n", - d ? d->name : "second"); } } @@ -283,8 +416,7 @@ display_panic_present(void) continue; panic_copy_console((volatile uchar *)P2V(paddr), w, h, stride); - if (drv->panic_present((struct pci_dev *)pci_get(i), paddr, w, h) == 0) - cprintf(" The report is on the %s display too.\n", drv->name); + drv->panic_present((struct pci_dev *)pci_get(i), paddr, w, h); } } diff --git a/kernel/dev/gfx.c b/kernel/dev/gfx.c index 0409bd5..bd70234 100644 --- a/kernel/dev/gfx.c +++ b/kernel/dev/gfx.c @@ -69,6 +69,41 @@ static void draw_glyph_px(int x, int y, uint32 cp, uint32 fg, uint32 bg); static struct fb_info scanout; static volatile uchar *fb; +// What has been drawn since anyone last asked. +// +// The console framebuffer is one screen on a machine that may have two, and +// the second one is a copy of this that somebody has to make. Copying all of +// it every time would be four megabytes to move one line of text, so the rows +// that were touched are remembered as a single span -- not a list of +// rectangles, because console output is lines and a span is what lines make. +// +// Rows rather than pixels: the copy is row-at-a-time anyway, and a column +// range would save nothing a memcpy cares about. +static int dmg_y0 = -1, dmg_y1 = -1; + +static void +damage(int y0, int y1) +{ + if (y0 < 0) y0 = 0; + if (y1 >= (int)scanout.height) y1 = (int)scanout.height - 1; + if (y1 < y0) return; + if (dmg_y0 < 0 || y0 < dmg_y0) dmg_y0 = y0; + if (dmg_y1 < 0 || y1 > dmg_y1) dmg_y1 = y1; +} + +// Hand the span over and forget it. Returns 0 when nothing has changed, which +// is the common case and the reason the caller can afford to ask often. +int +gfx_damage_take(int *y0, int *y1) +{ + if (dmg_y0 < 0) + return 0; + *y0 = dmg_y0; + *y1 = dmg_y1; + dmg_y0 = dmg_y1 = -1; + return 1; +} + static int cursor_x = 0; static int cursor_y = 0; // Current fill color for glyph backgrounds, backspace erase, and scroll -- @@ -125,6 +160,8 @@ gfx_draw_pixel(int x, int y, uint32 color) uint32 offset = (y * scanout.stride) + (x * (scanout.bpp / 8)); + damage(y, y); + if (scanout.bpp == 32) { *(volatile uint32*)(fb + offset) = color; } else if (scanout.bpp == 24) { @@ -404,6 +441,7 @@ gfx_scroll_up(uint32 bg) uint32 total_size = scanout.stride * scanout.height; memmove((void*)fb, (void*)(fb + line_size), total_size - line_size); + damage(0, (int)scanout.height - 1); // a scroll moves every row for (int y = (int)scanout.height - CHAR_HEIGHT; y < (int)scanout.height; y++) for (int x = 0; x < (int)scanout.width; x++) @@ -566,6 +604,8 @@ gfx_fill_rect(int x0, int y0, int w, int h, uint32 color) if (w <= 0 || h <= 0) return; + damage(y0, y0 + h - 1); + // Only the 32bpp case gets the fast path; anything else is rare enough that // correctness through gfx_draw_pixel is the better trade. if (scanout.bpp != 32) { diff --git a/kernel/dev/vt.c b/kernel/dev/vt.c index 91bfde3..c38e0a9 100644 --- a/kernel/dev/vt.c +++ b/kernel/dev/vt.c @@ -748,8 +748,8 @@ vt_csi(struct vt *v, int final) // Feed one output byte to a terminal. The serial side has already had the // raw byte; this is the framebuffer's interpretation of it. -void -vt_feed(struct vt *v, int c) +static void +vt_feed_1(struct vt *v, int c) { switch (v->esc_state) { case VE_ESC: @@ -898,3 +898,9 @@ vt_cycle(int dir) } return 0; // nowhere else to go; not an error } + +void +vt_feed(struct vt *v, int c) +{ + vt_feed_1(v, c); +} diff --git a/kernel/drivers/gpu/vga/vga_fb.c b/kernel/drivers/gpu/vga/vga_fb.c index b26f0fd..a227114 100644 --- a/kernel/drivers/gpu/vga/vga_fb.c +++ b/kernel/drivers/gpu/vga/vga_fb.c @@ -225,11 +225,22 @@ vga_disp_wait_vblank(struct device *d) break; } +// Whatever acquire() handed out, the pixels end up here: the linear +// framebuffer this driver published as the console's scanout. Saying so is +// what keeps the console mirror from copying the console onto itself. +static uint64 +vga_disp_scanout_phys(struct device *d) +{ + (void)d; + return fb_phys; +} + const struct display_ops vga_display_ops = { .mode = vga_disp_mode, .acquire = vga_disp_acquire, .present = vga_disp_present, .wait_vblank = vga_disp_wait_vblank, + .scanout_phys = vga_disp_scanout_phys, }; // Put the VBE framebuffer back on screen. diff --git a/kernel/drivers/gpu/virtio_gpu/virtio_gpu.c b/kernel/drivers/gpu/virtio_gpu/virtio_gpu.c index 284e050..7e4437d 100644 --- a/kernel/drivers/gpu/virtio_gpu/virtio_gpu.c +++ b/kernel/drivers/gpu/virtio_gpu/virtio_gpu.c @@ -402,11 +402,24 @@ vgpu_present(struct device *d, int x, int y, int w, int h) // No wait_vblank. The device presents whole rectangles atomically, so there is // nothing to synchronise against -- and a null op is more honest than one that // spins and achieves nothing. display_wait_vblank() handles the absence. +// The guest-side framebuffer, which is this device's own memory and not the +// console's -- on every machine this driver has run on. It is written as a +// comparison rather than as `return 0` because a machine whose *only* display +// is a virtio-gpu would have the console here, and then the honest answer +// changes without this line needing to. +static uint64 +vgpu_scanout_phys(struct device *d) +{ + struct vgpu *g = vgpu_of(d); + return (g && g->fb) ? dma_phys(g->fb) : 0; +} + const struct display_ops virtio_gpu_display_ops = { .mode = vgpu_mode, .acquire = vgpu_acquire, .present = vgpu_present, .wait_vblank = 0, + .scanout_phys = vgpu_scanout_phys, }; // --- probe -----------------------------------------------------------------