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 ljmp $(SEG_KCODE<<3), $start32
# Print null-terminated string at DS:SI via BIOS teletype (real mode only). # Print null-terminated string at DS:SI, to the screen *and* the serial port.
# Clobbers AX, BX, SI. # 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: print16:
.Lp16loop: .Lp16loop:
lodsb lodsb
testb %al, %al testb %al, %al
jz .Lp16done jz .Lp16done
pushw %ax
movb $0x0E, %ah movb $0x0E, %ah
movw $0x0007, %bx movw $0x0007, %bx
int $0x10 int $0x10
popw %ax
call serial_putc
jmp .Lp16loop jmp .Lp16loop
.Lp16done: .Lp16done:
ret 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. # Offer a resolution choice, briefly.
# #
# In real mode because this is the only place it can be: setting a VBE mode is # 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 # path, so whatever is chosen here is what the framebuffer, the console and the
# compositor all get. # 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. # 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. # In: CX = default mode. Out: CX = chosen mode. Clobbers AX, BX, DX, SI.
@ -146,30 +219,31 @@ res_menu:
movw $resprompt, %si movw $resprompt, %si
call print16 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 # seconds and needs no calibration against CPU speed -- a delay loop would be
# a different length on every machine. # a different length on every machine.
movb $0x00, %ah movb $0x00, %ah
int $0x1A # CX:DX = ticks since midnight int $0x1A # CX:DX = ticks since midnight
movw %dx, %bx # BX = starting tick, low word movw %dx, %bx # BX = starting tick, low word
.Lrwait: .Lrwait:
movb $0x01, %ah call anykey
int $0x16 # ZF=0 if a key is waiting jc .Lrkey
jnz .Lrkey
movb $0x00, %ah movb $0x00, %ah
int $0x1A int $0x1A
subw %bx, %dx subw %bx, %dx
cmpw $36, %dx cmpw $72, %dx
jb .Lrwait jb .Lrwait
movw $restimeout, %si
call print16
popw %cx # timed out: keep the default popw %cx # timed out: keep the default
ret ret
.Lrkey: .Lrkey:
xorw %ax, %ax
int $0x16 # consume it
orb $0x20, %al # crude tolower: 'R' and 'r' both land on 'r' orb $0x20, %al # crude tolower: 'R' and 'r' both land on 'r'
cmpb $'r', %al cmpb $'r', %al
je .Lrshow je .Lrshow
movw $restimeout, %si
call print16
popw %cx # some other key: boot with the default popw %cx # some other key: boot with the default
ret ret
@ -178,13 +252,22 @@ res_menu:
movw $resmenu, %si movw $resmenu, %si
call print16 call print16
.Lrpick: .Lrpick:
xorw %ax, %ax call anykey
int $0x16 jnc .Lrpick
cmpb $'1', %al cmpb $'1', %al
jb .Lrpick jb .Lrpick
cmpb $'3', %al cmpb $'3', %al
ja .Lrpick 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. # Index the table: (key - '1') * 2, because each entry is one 16-bit mode.
subb $'1', %al subb $'1', %al
movzbw %al, %bx movzbw %al, %bx
@ -214,7 +297,9 @@ resmodes:
.word 0x0145 # 1280x1024 verified .word 0x0145 # 1280x1024 verified
resprompt: resprompt:
.asciz "Press R for resolution... " .asciz "Press R within 4s to choose a resolution... "
restimeout:
.asciz "keeping 800x600\r\n"
resmenu: resmenu:
.asciz "\r\n 1) 800x600 2) 1024x768 3) 1280x1024\r\n choice: " .asciz "\r\n 1) 800x600 2) 1024x768 3) 1280x1024\r\n choice: "
resok: resok: