display: a 64-bit framebuffer address, no mirroring onto itself

Three fixes for real machines that broke at any resolution but 800x600:

- kmain() took the loader's framebuffer address as uint32, and
  vga_fb_init() re-published the scanout from that and the 16-bit VBE
  block. An aperture above 4 GB moved the console onto the wrong memory
  right after the vga: line. Both now use the loader's 64-bit display
  block, mapped with ioremap().
- The loader mirrored onto the console splitter's GOP, which forwards to
  the primary itself, and onto GOPs that share the primary's aperture.
  After a mode change that rewrote the screen over itself with another
  mode's stride. Handles without a device path and overlapping apertures
  are skipped, and a mode change re-matches the remaining mirrors.
- A mode whose reported size, stride or aperture does not add up after
  SetMode is refused, and the loader returns to the previous one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-23 15:46:42 +02:00
commit fe32833ade
5 changed files with 193 additions and 16 deletions

View file

@ -1212,3 +1212,46 @@ fallback while claiming not to be.
"A font is installed" and "a typeface was chosen" had been the same test
(`cfont == 0`) and quietly stopped being. They are two flags now, and
`gfx_font_ready()` answers the second one.
---
## 23. Every resolution but 800x600, on two real machines
Two reports in one afternoon. A desktop with an NVIDIA card booted fine at
800x600 and at anything else showed the kernel log as dotted lines across the
screen, and the loader's splash as four squashed copies of the logo side by
side. A laptop stopped dead after the kernel's `vga:` line. Three separate
defects were found under them; the third is a guard rather than a cure, and
the desktop has not yet been retested.
**The framebuffer address was cut in half.** The loader hands the kernel all
sixty-four bits of the aperture address in RBX, and `kmain()` took it as a
`uint32`. Below 4 GB that is harmless. Above it -- where a laptop's GPU is
entitled to put its aperture -- the low half is a plausible address pointing
at nothing. `kmain()` itself reads the loader's display block and puts the
early console on the right memory, but `vga_fb_init()` then re-published the
scanout from the truncated argument and the BIOS-era VBE block (32-bit
address, 16-bit sizes). The second publication won, and every line after
`vga:` went somewhere else. Both are fixed: `kmain(uint64)`, and
`vga_fb_init()` reads the same 64-bit block `kmain()` does and maps it with
`ioremap()` instead of `P2V()`.
**The loader mirrored the screen onto itself.** It copies its picture to
every other GOP it finds, for machines with two displays. But the firmware's
console splitter is itself a GOP, on a virtual handle, and its `Blt` forwards
to every real adapter -- the primary included. A laptop that reaches one panel
through two GOPs is the same thing with a shared aperture. Once the resolution
menu changed the primary's mode, the splitter still described the old one, so
each mirror pass wrote the primary's own pixels back over themselves at an
offset and with another mode's row length. Handles without a device path (the
splitter) and GOPs whose aperture overlaps the primary's are no longer mirror
targets, and a mode chosen in the menu re-matches the mirrors that remain.
QEMU's OVMF has the splitter too: `schos: 2 display adapter(s)`, both at the
same address.
**A mode the firmware cannot describe is refused.** After `SetMode`, the
loader now checks that the mode it got is the size it asked for, that the row
is at least as long as the width, and that the aperture holds that many rows.
If not, it goes back to the previous mode and says so in the menu. This proves
nothing about a mode that passes, but it stops a lying one from reaching the
kernel.