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>
71 lines
3 KiB
C
71 lines
3 KiB
C
#pragma once
|
|
|
|
// The display class: what a consumer talks to instead of a driver.
|
|
//
|
|
// A compositor should ask for "the display" and get whatever is bound --
|
|
// today the VBE framebuffer, later virtio-gpu. Without this, every consumer
|
|
// names a driver, and adding the second display driver means editing all of
|
|
// them. That is not hypothetical here: the framebuffer's physical address was
|
|
// once hardcoded in three separate places.
|
|
//
|
|
// Found with device_find_class(DEVCLASS_DISPLAY); see include/device.h.
|
|
|
|
struct device;
|
|
|
|
struct display_mode {
|
|
int width, height;
|
|
int bpp;
|
|
uint32 stride; // bytes per row of the *visible* framebuffer
|
|
};
|
|
|
|
struct display_ops {
|
|
void (*mode)(struct device *, struct display_mode *);
|
|
|
|
// The buffer to draw into, and its stride. Returns 0 if double buffering is
|
|
// unavailable, in which case the caller should draw directly and accept
|
|
// tearing rather than not drawing at all.
|
|
//
|
|
// Allocated on first call, never at boot: it is around two megabytes at
|
|
// 800x600, and a machine whose compositor never starts should not pay that.
|
|
void *(*acquire)(struct device *, uint32 *stride_out);
|
|
|
|
// Copy a rectangle of the back buffer to the display. Coordinates are in
|
|
// pixels; the whole screen is (0, 0, width, height).
|
|
void (*present)(struct device *, int x, int y, int w, int h);
|
|
|
|
// Block until the display is between frames. Optional -- a driver that
|
|
// 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().
|
|
struct device *display_get(void);
|
|
|
|
// Wrappers that do nothing gracefully when there is no display or the driver
|
|
// left an op null, so callers are not each written around a maybe-null vtable.
|
|
int display_mode(struct display_mode *out);
|
|
void *display_acquire(uint32 *stride_out);
|
|
void display_present(int x, int y, int w, int h);
|
|
void display_wait_vblank(void);
|
|
|
|
// Drive the whole path once and draw a test pattern. `echo d > /proc/sysrq`.
|
|
void display_selftest(void);
|
|
|
|
// Draw a text specimen through the display class. `echo t > /proc/sysrq`.
|
|
void text_selftest(void);
|