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:
parent
e72205cdc0
commit
182cc76588
14 changed files with 437 additions and 42 deletions
21
NOTICE
21
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.
|
||||
|
||||
================================================================================
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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().
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 -----------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue