VBE in the toy VM: what the corpus asks for, and what we answer
tools/toyvm/dos.js carries a small VBE 1.2 — the VESA BIOS Extension half of
int 10h. This file is the measurement behind it: which programs in
/tmp/demos call it, which modes they want, and which of those we serve.
The tool is tools/toyvm/vbe-census.js; read its header for how it runs. In
one line: one child process per program, 150M dispatches each with the sweep's
own flags (--pit-clock --auto-key --sound-pref=sb), recording every
AX=4Fxx call, the mode number in CX/BX, whether the linear-framebuffer bit
was set, whether we granted it, and what the program did next.
Why the earlier number was wrong
dos-corpus-blockers.md said "How many rows want
VESA: three — every program was run for 4 seconds with --trace-int". Four
seconds is not a budget, it is a stopwatch on a loaded box, and it undercounts
by construction. CHROME.EXE is the proof: its VBE query sits behind a
retrace-paced text scroller and does not happen until roughly 100M dispatches.
A run that stops before the question is asked reports that the program never
asks it.
At 150M dispatches the answer is five, not three, and the new name — COUNTDWN.EXE — was already being served.
The census, 199 programs
The five that call VBE at all
| program | functions | mode info (4F01) | set mode (4F02) | LFB | outcome |
|---|---|---|---|---|---|
| AQUAPHOB.EXE | 4F00 4F01 4F02 4F05 | 101 | 101 | - | vesa+drew |
| CHROME.EXE | 4F01 4F02 4F07 | 112 | 112 | - | vesa+drew |
| COLORS.EXE | 4F01 4F02 | 101 | 10d | - | vesa+drew |
| COUNTDWN.EXE | 4F00 4F01 4F02 4F05 | 101 | 101 | - | vesa+drew |
| SETUP.EXE | 4F00 4F01 | 101 | - | - | blank |
(CHROME and COLORS read exited blank and refused-10d before this change;
the table is the state after it. SETUP.EXE — the Angel/Byetro installer — asks
4F00 and 4F01 for 0x101 and then never sets a mode; it is a setup program with
another blocker in front of it, and no mode we could add changes that row.)
Modes asked for, by how many programs
| mode | what it is | served | programs | who |
|---|---|---|---|---|
| 0x101 | 640x480x8 | yes | 4 | AQUAPHOB.EXE COLORS.EXE COUNTDWN.EXE SETUP.EXE |
| 0x10D | 320x200x15 | new | 1 | COLORS.EXE |
| 0x112 | 640x480x24 | new | 1 | CHROME.EXE |
No program in the corpus asks for a linear framebuffer. Not one 4F01 or 4F02 carried bit 0x4000. So the LFB path stays refused, and the refusal is tested rather than assumed — see below.
Functions used
| function | what it is | programs | calls |
|---|---|---|---|
| 4F01 | mode info | 5 | 5 |
| 4F02 | set mode | 4 | 4 |
| 4F00 | controller info | 3 | 4 |
| 4F05 | window control | 2 | 4705 |
| 4F07 | display start | 1 | 1 |
Nothing in the corpus calls 4F03, 4F04, 4F06, 4F08 or 4F09.
What was implemented
Three things, and only the first is a mode.
Direct colour, and two modes in it. VESA_MODES carries a depth now, and
framebuffer.js reads 15/16/24/32bpp pictures as well as 8bpp: readDirect
widens 5-5-5 and 5-6-5 through lookup tables and reads 24bpp as BGR triples.
4F01 fills the VBE 1.2 direct-colour mask fields at 0x1F..0x26 and reports
memory model 6 rather than 4, because a program that reads model 4 on a 24bpp
mode goes looking for a palette that is not in the path. Mode 0x112
(640x480x24) is CHROME's; mode 0x10D (320x200x15) is COLORS's. Both were
added with their render path, and test/test-toyvm-vbe.js checks each one by
writing known pixels through the window and reading them back out of the frame.
AX=4F07, display start. CHROME calls it with (0,0) immediately after
setting its mode. It is not decoration: leaving AX unchanged there told the
program a call every VBE 1.2 card answers had been refused. It is implemented
as a real offset — the surface's start comes from it, so a program that pans
sees the picture move — and a start that would run the visible window off the
end of the 1MB picture is refused rather than clamped.
The window-positioning far call, ModeInfoBlock+0x0C. This is the one that
mattered most and was the least visible. A program may move the window by
CALL FAR through that pointer instead of AX=4F05; CHROME does, at
100:0afd (call far cs:[0b7c], the pointer it copied out of the block). We
had been writing 0000:0000 there. That does not make a program fall back on the
interrupt — it far-calls the interrupt vector table and executes it. CHROME
ended up spinning at 0:18c with its stack unwound. The fix is eight bytes of
real code below the PSP (push ax / mov ax,4F05 / int 10h / pop ax / retf),
which cannot live in the F000 stub segment because the run loop reads any CS of
F000 as "a vector was taken".
What was declined, and why
The linear framebuffer. 4F01 with CX & 0x4000 and 4F02 with BX & 0x4000
are refused, and PhysBasePtr is not offered. The picture lives at
isa.VESA_FB, outside the guest's address space, and there is no physical
address the guest could write it through — granting the bit would hand a program
an address it would then draw into nothing. No program in this corpus wants
it, so this costs a row nothing today.
Modes 0x100, 0x103, 0x105 and everything else stay as they are: nobody asks.
The rule the test enforces is the reason to keep the list short — 4F01 must
refuse a mode we cannot render (test/test-toyvm-vbe.js asks about 0x114 and
requires a failure), because a "supported" answer for a mode the frame reader
cannot read is a silent-success stub: the program sets it, draws into the
window, and the PNG, the frame hash and the live page all show nothing, with no
call anywhere saying otherwise.
CHROME.EXE, before and after
Same command line (--dispatches=150m --pit-clock --auto-key):
| before | after | |
|---|---|---|
| ends | exited=true code=0 at 100.4M |
still running at 150M |
| video mode | 3h 320x200, 0 non-black of 64000 |
112h VBE 640x480x24, 89560 non-black of 307200 |
| on screen | the warning, then To Be Continued... |
the raytraced CHROME logo |
At 20M and 60M dispatches both arms are still in the demo's own text intro ("WARNING! INTRO CONTAINS REALTIME RAYTRACING… RUNNING_"), which is the show, not a refusal — the VBE question is not asked until about 100M.
COLORS.EXE goes from exited blank to its 15bpp title screen ("COLORS —
Wonders of the a-men").
What did not move
AQUAPHOB.EXE and COUNTDWN.EXE, the two 0x101 users, are unchanged frame hash for
frame hash (f9aaf10b and 9fdddade in both arms at --dispatches=80m). The
six witnesses DADEMO3 / RUNDEMO / BLIQ / ACME-BIG / CONTAGIO / CATWALK are
byte-identical, frame PNG and WAV, against a main base arm.
sweep-dos.js --dir=/tmp/demos on both arms, through sweep-diff.js, over 191
compared programs: 0 regressions, 0 went blank, 190 unchanged, 1 changed —
COLORS.EXE: frame 38c165c5 -> 7eb1ed94, 2.5M -> 8.0M dispatches (x3.23), px 0 -> 28081
which is the whole intended effect: it stops exiting on the refusal and runs on into its title screen, so it spends the sweep's full budget instead of 2.5M.
CHROME.EXE is unchanged in that sweep and that is not a contradiction. The
sweep photographs each program at 8.0M dispatches and CHROME does not ask its
VBE question until about 100M, so both arms are still in its text intro with 0
pixels lit. It is the same budget artefact that produced the "three programs"
figure in the first place — which is why the row that proves this change is the
150M run above, not the sweep.