danos/docs/os-development/gop.md

71 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# GOP
Graphics Output Protocol (GOP) is a UEFI driver interface that replaces legacy VGA BIOS functions to provide graphics console output in the pre-OS phase.
It allows the firmware to display boot screens and setup menus by providing direct access to the hardware frame buffer, enabling multiple GPUs to function equally without proprietary INT15 handshaking.
GOP has no standardized mode numbers. Instead, the firmware (really the GPU's GOP driver) builds a list of modes at boot, and each is just an opaque index 0 .. MaxMode-1. Mode 0 might be 1920×1080 on your laptop and 800×600 on someone else's — the index carries no fixed meaning. To learn what a mode actually is, you have to ask:
- gop.mode.max_mode — how many modes exist
- gop.query_mode(n) — returns the info (resolution, pixel format, pixels_per_scan_line) for mode n
- gop.set_mode(n) — switch to it
- gop.mode.info — the info for the currently active mode
queryFramebuffer used to read gop.mode.info directly and never call set_mode — taking whatever mode the firmware selected as its default. It now works out the monitor's native resolution from EDID and, if a matching mode exists, calls set_mode to switch to it. When EDID is unavailable it keeps the firmware's default mode rather than guessing — with a valid EDID present the firmware normally defaults to the native mode itself (this is exactly how the QEMU run boots at 1280x720: OVMF's stdvga driver reads the emulated EDID and defaults to the preferred mode, even though it never exposes the EDID protocols to us). See the "picks the native resolution" note at the bottom.
Does EFI detect native resolution?
Sometimes, but it's not guaranteed by the spec. Here's the actual chain:
1. The GOP driver reads the monitor's EDID over the DDC/I²C wire — a data blob the display publishes describing its supported resolutions, including its preferred (native) timing.
2. The firmware then picks a default GOP mode. Many modern firmwares (and OVMF in a VM, driven by the emulated display) do default to the native/preferred resolution. But plenty of firmware defaults to a safe fallback like 1024×768 or 800×600 regardless of what the panel can do.
So gop.mode.info giving you native res is a common outcome, not a promise. Two more caveats:
- The mode list itself may not even contain the true native resolution — a GOP driver can expose only a handful of modes.
- On a headless VM (your QEMU/OVMF setup), there's no real EDID; the "native" resolution is whatever the emulated GPU advertises. OVMF's default is typically 800×600 or 1024×768 unless you configure it (e.g. QEMU's -device virtio-vga with a set resolution, or the OVMF Platform config).
One note worth flagging: this picks the native resolution but doesn't force a particular pixel format — a native mode is only chosen if it's a paintable linear 32bpp layout (RGBX/BGRX); a bit_mask/blt_only native mode is skipped and we keep the firmware's default instead. That's the right trade-off for now since your console assumes linear 32bpp.
## Pixel formats
Every mode also carries a pixel format, and queryFramebuffer only accepts two of the four GOP formats. The full enum:
- red_green_blue_reserved_8_bit_per_color (RGBX) — accepted
- blue_green_red_reserved_8_bit_per_color (BGRX) — accepted
- bit_mask — rejected
- blt_only — rejected
RGBX/BGRX tell you each pixel is a 32-bit value with a fixed byte order. That's what the console needs: a known layout it can write directly with rowPtr(y)[x] = color. The other two break one of those assumptions.
### bit_mask (spec: PixelBitMask)
The framebuffer is still linear memory you can write to directly — but the bits aren't in a standard RGBX/BGRX arrangement. Instead the firmware hands you a PixelBitmask struct describing where each channel lives:
```zig
pub const PixelBitmask = extern struct {
red_mask: u32,
green_mask: u32,
blue_mask: u32,
reserved_mask: u32,
};
```
Each mask marks which bits of the pixel word belong to that channel. This is how the format expresses non-standard layouts — for example 16-bit RGB565 (red = 0xF800, green = 0x07E0, blue = 0x001F: 5/6/5 bits, only 16 bits per pixel), or an odd 32-bit order. To draw a color you'd have to read the masks, work out each channel's bit position and width, shift/scale your 8-bit R/G/B into place, and OR them together — per pixel. It's fully drawable, just not with a hardcoded 32-bit write, so we reject it rather than carry that machinery.
(RGBX/BGRX are really just two hardcoded special cases of a bitmask. The spec names them separately precisely so simple loaders can skip mask-decoding in the common case.) In practice bit_mask is rare on modern PC firmware — you'll almost always get RGBX or BGRX — so rejecting it costs basically nothing.
### blt_only (spec: PixelBltOnly)
This one is more fundamental: there is no linear framebuffer you can address at all. gop.mode.frame_buffer_base is meaningless — you have no pointer to pixel memory. The only way to put pixels on screen is through GOP's Blt ("block transfer") service, the third function pointer in the protocol: you build pixels in your own buffer and ask the firmware to copy ("blit") a rectangle onto the display. The firmware owns the actual scanout memory, wherever it lives (across a bus, behind a GPU command interface, in a layout the CPU can't map directly).
The catch that matters for us: Blt is a boot service. It stops working the instant you call ExitBootServices — which is exactly when the kernel runs. So a blt_only display gives a post-exit kernel no way to draw pixels at all, and there's genuinely nothing our framebuffer console could do with it. Rejecting it is the only correct response.
### Summary
| Format | Linear memory? | Layout | Console |
| --- | --- | --- | --- |
| rgbx / bgrx | yes | fixed 32bpp byte order | works — direct writes |
| bit_mask | yes | arbitrary, described by masks | rejected — would need per-pixel mask decoding |
| blt_only | no | no CPU-visible framebuffer; Blt service only | rejected — and Blt is gone after ExitBootServices anyway |
So the two-case accept list is the right line to draw: RGBX/BGRX are the only formats that give a post-ExitBootServices kernel a flat block of pixel memory it can write to without help from firmware that no longer exists.