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 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-09-22 21:47:35 +02:00
commit 182cc76588
14 changed files with 437 additions and 42 deletions

21
NOTICE
View file

@ -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.
================================================================================

View file

@ -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;

View file

@ -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.

View file

@ -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

View file

@ -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,

View file

@ -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().

View file

@ -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);

View file

@ -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;

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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);
}

View file

@ -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.

View file

@ -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 -----------------------------------------------------------------