boot: make the resolution prompt reachable from the serial console

The menu shipped working only for someone driving the emulated keyboard,
which is nobody. `make qemu` runs -serial mon:stdio, so the terminal the
developer is sitting in front of is the serial line -- and the menu read
INT 16h, which is the PS/2 keyboard. Keys went nowhere.

The prompt was invisible for a second reason: BIOS teletype writes to the
VGA text screen, and `make qemu` attaches virtio-gpu alongside stdvga and
shows that one, so the text landed on a display nobody was looking at.

So print16 now writes to both the screen and COM1, and the menu accepts a
key from either source. The transmitter wait is bounded, because a machine
with no serial port reads 0xFF from the status register forever and would
otherwise hang the loader on exactly the hardware that cannot say why.

Also: four seconds rather than two, which is not long when the prompt has
to be read first; the chosen digit is echoed, because a serial line has no
local echo and the menu otherwise looks like it ignored the key; and the
timeout says "keeping 800x600" instead of silently moving on.

Verified three ways: typing r then 2 over serial gives 1024x768, sendkey r
then 3 on the keyboard gives 1280x1024, and no key at all prints the
timeout and boots 800x600. bootcheck 108 [ok].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-08-10 05:02:10 +02:00
commit 8757846a9b

View file

@ -115,20 +115,93 @@ seta20.2:
ljmp $(SEG_KCODE<<3), $start32
# Print null-terminated string at DS:SI via BIOS teletype (real mode only).
# Clobbers AX, BX, SI.
# Print null-terminated string at DS:SI, to the screen *and* the serial port.
# Clobbers AX, BX, DX, SI.
#
# Both, because neither one alone is where the person is looking. BIOS teletype
# goes to the VGA text screen -- but `make qemu` attaches virtio-gpu alongside
# stdvga and shows that one, so the text lands on a display nobody sees. And
# `make qemu` puts the serial line on stdio, which is the terminal the developer
# is actually sitting in front of.
print16:
.Lp16loop:
lodsb
testb %al, %al
jz .Lp16done
pushw %ax
movb $0x0E, %ah
movw $0x0007, %bx
int $0x10
popw %ax
call serial_putc
jmp .Lp16loop
.Lp16done:
ret
# Write AL to COM1, preserving AX.
#
# The wait for the transmitter is bounded. A machine with no serial port reads
# 0xFF from the status register forever, and an unbounded wait there would hang
# the loader on exactly the hardware that cannot tell you why.
serial_putc:
pushw %ax
pushw %cx
pushw %dx
movb %al, %ah # stash the character; AL is about to be an input
movw $0xFFFF, %cx
movw $0x3FD, %dx # LSR
.Lspw:
inb %dx, %al
testb $0x20, %al # transmitter holding register empty
jnz .Lspgo
loop .Lspw
jmp .Lspdone # never drained: no port, or nothing listening
.Lspgo:
movb %ah, %al
movw $0x3F8, %dx
outb %al, %dx
.Lspdone:
popw %dx
popw %cx
popw %ax
ret
# CF=1 and AL=byte if COM1 has one waiting, CF=0 otherwise. Clobbers AX, DX.
serial_getc:
pushw %dx
movw $0x3FD, %dx # LSR
inb %dx, %al
testb $0x01, %al # data ready
jz .Lsgnone
movw $0x3F8, %dx
inb %dx, %al
popw %dx
stc
ret
.Lsgnone:
popw %dx
clc
ret
# CF=1 and AL=key from either the BIOS keyboard or the serial line.
# Clobbers AX, DX.
#
# Both, because the menu has to answer whoever is typing. Under `make qemu` the
# serial line is stdio -- the terminal the developer is in -- while a real
# machine, or a QEMU window, has a keyboard and no serial at all. Listening to
# only one is how this shipped "working" and did nothing for the person using it.
anykey:
movb $0x01, %ah
int $0x16 # ZF=0 if the keyboard has one
jz .Lakserial
xorw %ax, %ax
int $0x16 # consume it
stc
ret
.Lakserial:
call serial_getc
ret
# Offer a resolution choice, briefly.
#
# In real mode because this is the only place it can be: setting a VBE mode is
@ -137,7 +210,7 @@ print16:
# path, so whatever is chosen here is what the framebuffer, the console and the
# compositor all get.
#
# Two seconds, then it boots on its own. A loader that waits for a keypress is
# Four seconds, then it boots on its own. A loader that waits for a keypress is
# a machine that does not come back up after a power cut.
#
# In: CX = default mode. Out: CX = chosen mode. Clobbers AX, BX, DX, SI.
@ -146,30 +219,31 @@ res_menu:
movw $resprompt, %si
call print16
# Poll for ~2s. INT 1Ah tick count runs at 18.2 Hz, so 36 ticks is two
# Poll for ~4s. INT 1Ah tick count runs at 18.2 Hz, so 72 ticks is four
# seconds and needs no calibration against CPU speed -- a delay loop would be
# a different length on every machine.
movb $0x00, %ah
int $0x1A # CX:DX = ticks since midnight
movw %dx, %bx # BX = starting tick, low word
.Lrwait:
movb $0x01, %ah
int $0x16 # ZF=0 if a key is waiting
jnz .Lrkey
call anykey
jc .Lrkey
movb $0x00, %ah
int $0x1A
subw %bx, %dx
cmpw $36, %dx
cmpw $72, %dx
jb .Lrwait
movw $restimeout, %si
call print16
popw %cx # timed out: keep the default
ret
.Lrkey:
xorw %ax, %ax
int $0x16 # consume it
orb $0x20, %al # crude tolower: 'R' and 'r' both land on 'r'
cmpb $'r', %al
je .Lrshow
movw $restimeout, %si
call print16
popw %cx # some other key: boot with the default
ret
@ -178,13 +252,22 @@ res_menu:
movw $resmenu, %si
call print16
.Lrpick:
xorw %ax, %ax
int $0x16
call anykey
jnc .Lrpick
cmpb $'1', %al
jb .Lrpick
cmpb $'3', %al
ja .Lrpick
# Echo the digit: over serial there is no local echo, so without this the
# menu looks like it ignored the key.
call serial_putc
pushw %ax
movb $0x0E, %ah
movw $0x0007, %bx
int $0x10
popw %ax
# Index the table: (key - '1') * 2, because each entry is one 16-bit mode.
subb $'1', %al
movzbw %al, %bx
@ -214,7 +297,9 @@ resmodes:
.word 0x0145 # 1280x1024 verified
resprompt:
.asciz "Press R for resolution... "
.asciz "Press R within 4s to choose a resolution... "
restimeout:
.asciz "keeping 800x600\r\n"
resmenu:
.asciz "\r\n 1) 800x600 2) 1024x768 3) 1280x1024\r\n choice: "
resok: