Commit graph SchOS/boot/bios
Author SHA1 Message Date
Nexoniarz
879db62379 boot: stage2 climbs to long mode, and both loaders use one kernel entry
The BIOS loader handed the kernel a 32-bit entry that climbed into long
mode itself; the UEFI loader arrived already there and used a separate
64-bit entry. Two entries meant two copies of the same preconditions, and
that is not a hypothetical cost -- it is exactly where EFER.SCE went
missing for months, set on one path and not the other, invisible until a
program tried to return from a syscall.

stage2 does the climb now. It builds the page tables where the UEFI
loader builds them and meaning the same thing -- 4 GB identity-mapped
with 1 GB pages, and the same 4 GB again at 0xffff800000000000, because
entry64low's first instruction is a movabs to a high address and identity
alone triple faults on it -- sets PAE, LME and SCE, enables paging, and
far jumps through a GDT with an L-bit code descriptor, which a 32-bit
descriptor cannot express.

stage2main.c is compiled for x86_64 and runs in long mode. It still reads
the multiboot header to find where the kernel's segments go, but enters
through the SCH64 header rather than the entry named there: that entry is
32-bit code whose only job is the climb, and dropping back to 32 bits to
redo work already done is a mode switch for no reason. If the SCH64
header is absent it halts, because there is no longer anything to fall
back to.

So both loaders now hand over identically -- long mode, interrupts off,
CR3 mapping the low 4 GB twice, framebuffer in RBX -- and there is one
set of preconditions instead of two that can drift.

Verified on every path that exists: BIOS to schinit; BIOS with -smp 2,
which brings up cpu1 through the AP trampoline's own separate climb; the
resolution menu still selecting 1024x768 over serial; the ISO, which
loads through ATAPI and a ramdisk root; UEFI unchanged at 1018 MB in 7
regions; bootcheck 108 [ok]. stage2 is 21988 of 24576 bytes.

The 32-bit mboot_entry stays in entry.S. Nothing of ours uses it now, but
it is what makes the kernel loadable by a multiboot loader that is not
ours, and removing it is a separate decision from this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 22:09:09 +02:00
Nexoniarz
0500e4382d boot: one hand-off contract, defined once
Groundwork for the unified loader: before the two paths can share code,
they have to agree on what they are producing, and until now they only
agreed by coincidence.

Every field of the loader/kernel hand-off was #defined in whichever file
happened to need it -- kernel/core/main.c, kernel/dev/vga.c,
kernel/arch/x86_64/e820.c, boot/bios/cdboot.S, boot/bios/stage2main.c and
boot/uefi/main.c, six copies of the same addresses. schboot.h existed for
exactly this and held only the entry magic and the RSDP slot, with a
comment describing the rest of the block it did not define.

The whole block lives there now: the VBE info fields, the verbose and
media flags (with the real-mode segment:offset spelling cdboot.S needs),
the e820 count, map and cap, and the RSDP. Every one of those files
includes it instead of restating it.

This is not a tidy-up. It is the same hazard as defining the entry magic
twice, only quieter -- nothing breaks until one copy moves, and the two
loaders diverging is precisely what produced the EFER.SCE and RSDP bugs,
where a fix on one path silently did not exist on the other.

Verified on both firmwares: BIOS boots to schinit with the same e820,
ACPI and VGA lines as before; UEFI reaches cpu0 with 1018 MB in 7 regions
and its own 1280x800 mode. `make iso` still builds. bootcheck 108 [ok].

Next: the loaders become starters that gather firmware-specific facts and
hand to one common stage. That needs the BIOS side to climb to long mode
first, since common code can only be compiled for one CPU mode -- which
is the risky part, and is why the contract went first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 21:37:36 +02:00
Nexoniarz
bf9ac843f7 schcom: repaint what changed, and stop rendering frames nobody sees
Moving the mouse repainted the whole screen. At 1280x800 that is a
million pixels of desktop fill, every surface re-blitted, and a 4 MB copy
to the framebuffer -- per event. Under emulation the pointer lagged the
mouse by whole seconds, and that was not the emulation's fault: moving a
10x10 cursor does not require touching four megabytes.

Two changes, and the second matters as much as the first.

Damage tracking: one rectangle, unioned from whatever actually changed,
and only those rows are redrawn and copied out. A pointer move damages
where it was and where it is; a commit damages that surface; a client
going away damages what it had. One rectangle rather than a list, because
in practice the changes are a cursor and the window under it, and a list
costs more bookkeeping than it saves.

Draining input: a single mouse movement produces a burst of EV_REL ending
in EV_SYN, and the loop handled one event per poll and repainted. So the
compositor rendered every intermediate position of a cursor that was
never seen at those positions. Both queues are now drained before
anything is painted. handle_input grew a distinct "queue empty" return
for it -- EV_SYN changes nothing but is not the end of the burst, and a
drain that stopped there would leave the rest queued.

Also cuts the loader's resolution prompt from four seconds to one and a
half, on both the BIOS and UEFI paths. Four was mine and it was too long:
it is paid on every unattended boot, and adding four seconds to every
start to offer a menu nobody usually wants makes the common case worse to
serve the rare one.

schcom-test passes, including two overlapping clients and stacking order.
bootcheck 108 [ok].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 05:54:06 +02:00
Nexoniarz
03a867f43e boot: enumerate VBE modes, so a resolution can be typed
Option 4 asks for a width and a height and then goes looking for a mode
that matches, instead of picking from three hardcoded numbers.

AX=4F00 returns a controller info block whose VideoModePtr is a far
pointer to a 0xFFFF-terminated list of mode numbers; each is asked what it
actually is with AX=4F01, and the first that reports the requested size at
32bpp wins. That is the fix for the table, not an addition to it -- the
table was wrong the first time it was written (0x146 was "1600x1200" and
is 320x200x8 here), and a table can only ever describe machines somebody
happened to test.

800x600 stays the floor: the console, the splash and Schcom all assume at
least that, and smaller would be a different set of assumptions rather
than a smaller number. Below it, or a size the BIOS cannot do, says so and
keeps 800x600 rather than failing to boot.

Scratch buffers at 0x31000 and 0x31200, clear of the mode info block at
0x30000 and everything else in that page -- the flags at +0x100, the e820
map at +0x208 and the RSDP slot at +0x600.

Verified over serial: 1280x800 comes up at 1280x800, which is in no table
anywhere and is the point; 640x480 is refused as below the minimum;
3000x2000 reports no such mode. Presets 1-3 and the no-key timeout are
unchanged. bootcheck 108 [ok]. stage2 is 20932 of 24576 bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 05:13:53 +02:00
Nexoniarz
8757846a9b 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>
2026-08-10 05:02:10 +02:00
Nexoniarz
8b52782b75 boot: pick a resolution with R, instead of hardcoding 800x600
The VBE mode was the literal 0x143 in stage2.S, so 800x600 was the only
resolution the system could ever have. Real mode is the only place this
can be chosen: setting a mode is INT 10h, stage2main.c already runs in
protected mode, and the kernel has no modeset path -- so whatever is
picked here is what the framebuffer, the console and Schcom all get.

Press R at the prompt within two seconds for 800x600, 1024x768 or
1280x1024. It boots on its own otherwise: a loader that waits for a
keypress is a machine that does not come back up after a power cut.

The depth is checked before the mode is set, and that is not belt and
braces. 0x146 was in the table as 1600x1200x32 and this VBE returns
320x200x8 for it -- a mode the BIOS accepts, so an error return never
happens. It boots and then everything above renders garbage, because the
console, the fbdev ioctls and the compositor all assume 32bpp. Only
numbers verified to come back as the advertised size at 32bpp are listed
now, and anything else falls back to 800x600.

Verified by driving the monitor's sendkey: 1, 2 and 3 come up at
800x600, 1024x768 and 1280x1024 respectively, all reaching schinit, and
no key at all still gives 800x600. bootcheck 108 [ok], schcom-test
passes.

Typing an arbitrary size is the obvious next step and deliberately not
guessed at here: it wants enumerating the BIOS's own mode list
(INT 10h AX=4F00) and matching on width and height, which would replace
the magic numbers rather than add more of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 04:47:27 +02:00
Nexoniarz
08a1a9033a boot: separate BIOS from UEFI
boot/bios/ now holds the MBR, the El Torito stub and the real-mode stage
2; boot/uefi/ is empty and documented.

Separate directories rather than #ifdefs, because the two share almost
nothing. A BIOS loader lives in 16-bit real mode, reaches the firmware
through software interrupts, and walks itself up into long mode. A UEFI
loader is a 64-bit PE that the firmware has already placed in long mode
with paging on, calling services through a table of function pointers.
What they do share is the hand-off contract, which boot/README.md now
writes down: ELF segments placed, long mode, a 24-byte-entry memory map
at E820_MAP_ADDR, framebuffer geometry at VBE_INFO_ADDR.

That contract is the point of documenting it now. UEFI's GetMemoryMap
returns EFI_MEMORY_DESCRIPTORs, and the right move is to translate them
into the entries kernel/arch/x86_64/e820.c already parses rather than
teach the kernel a second format.

Build output paths under $(BUILD)/boot are unchanged; only sources moved.
2026-08-09 16:00:03 +02:00