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
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue