Skip to content

Commit 766497b

Browse files
v86: defer V86-mode VBE disable; add DOS-box probe scenario (#359)
Rebuilds libv86.js from felixrieseberg/v86@windows95-base, which now carries vga-defer-vbe-disable-v86: when a windowed DOS VM's vgabios writes dispi[4]=0, Win9x's VDD passes that through (it doesn't know about ports 1CE/1CF) while virtualising the rest of the mode-set, so v86 used to drop out of LFB rendering with the legacy registers still holding SVGA values and the screen turned to planar garbage. The fix defers the disable until a legacy attribute-mode write actually reaches the hardware. debug-harness: WIN95_PROBE_DOSBOX=1 opens command from Run, types dir, optionally Alt+Enters (WIN95_PROBE_DOSBOX_ALTENTER=1). WIN95_PROBE_VGATRACE=1 wraps the VGA io.ports[] entries (not the VGAScreen methods, which are captured by-value at registration) and dumps [port, op, value, eip+VM/PE/CPL] tuples to /tmp/win95-vgatrace.json — that EIP/mode column is what pinned the leak on V86-mode vgabios at C000:2C8x.
1 parent 85c4451 commit 766497b

4 files changed

Lines changed: 143 additions & 16 deletions

File tree

.claude/skills/probe-win95/SKILL.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ WIN95_SMB_SHARE="$HOME/Downloads" \
3737
```
3838

3939
`WIN95_PROBE_SCRIPT='HOST/HOST'` types `\\HOST\HOST` into Start → Run on
40-
desktop. `/``\` substitution (env var / shell quoting, pragmatism). The
40+
desktop. `WIN95_PROBE_DOSBOX=1` instead opens `command`, types `dir`,
41+
and (with `WIN95_PROBE_DOSBOX_ALTENTER=1`) toggles fullscreen — this is
42+
the regression scenario for the windowed-DOS-box VBE leak.
43+
`WIN95_PROBE_VGATRACE=1` wraps the VGA I/O ports at the `io.ports[]`
44+
layer and writes `[port, op, value, "eip VMPE cplN"]` tuples to
45+
`/tmp/win95-vgatrace.json` every tick (heavy — can hit 1M entries during
46+
boot). `/``\` substitution (env var / shell quoting, pragmatism). The
4147
harness drives it via XT scancodes — Win95 doesn't have Win+R (Win98+
4248
only), so the sequence is Esc, Esc, Ctrl+Esc, R, backslashes + text,
4349
Enter.

.claude/skills/update-v86/SKILL.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ fallbacks, no fetching from copy.sh.
2323
## The fork branch
2424

2525
v86 should be checked out on **`felixrieseberg/v86:windows95-base`**.
26-
That branch merges three feature branches, each upstreamable on its own:
26+
That branch merges four feature branches, each upstreamable on its own:
2727

2828
- **`electron-renderer-fs-loader`** (PR #1540) — `src/lib.js` uses
2929
`require("fs")` instead of `await import("node:fs/promises")`. Dynamic
@@ -40,6 +40,13 @@ That branch merges three feature branches, each upstreamable on its own:
4040
(VBADOS VBMOUSE) can read absolute cursor position and track the host
4141
cursor 1:1 without pointer lock. Consumes the `mouse-absolute` bus
4242
event that `MouseAdapter` already emits.
43+
- **`vga-defer-vbe-disable-v86`**`src/vga.js` defers `dispi[4]=0`
44+
written from V86 mode until a legacy attribute-mode write reaches the
45+
hardware. Win9x's VDD virtualises ports 3B0–3DF for a windowed DOS VM
46+
but not 1CE/1CF, so vgabios's VBE-disable leaks through while the rest
47+
of its mode-set is captured into the VM's virtual register file —
48+
without this the screen turns to planar garbage the moment you open a
49+
DOS box.
4350

4451
## Prerequisites
4552

src/renderer/debug-harness.ts

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,78 @@ const SC = {
3636
R_DN: [0x13], R_UP: [0x93],
3737
ENTER_DN: [0x1c], ENTER_UP: [0x9c],
3838
BACKSLASH_DN: [0x2b], BACKSLASH_UP: [0xab],
39+
ALT_DN: [0x38], ALT_UP: [0xb8],
3940
};
4041

42+
// WIN95_PROBE_VGATRACE=1 → wrap VGA I/O ports at the io.ports[] layer (the
43+
// VGAScreen.portXXX_write methods are captured by-value at registration time,
44+
// so monkey-patching them on the instance is a no-op for most ports). Each
45+
// entry is [port, op, value, "eip VMPE cplN"] so you can tell vgabios in V86
46+
// mode apart from the ring-0 display driver.
47+
const VGATRACE_FILE = "/tmp/win95-vgatrace.json";
48+
let vgaTrace: any[] | undefined;
49+
50+
function armVgaTrace(emulator: any) {
51+
const cpu = emulator.v86?.cpu;
52+
const io = cpu?.io;
53+
if (!io || vgaTrace) return;
54+
vgaTrace = [];
55+
const ctx = () => {
56+
try {
57+
const ip = (cpu.instruction_pointer[0] >>> 0).toString(16);
58+
const vm = cpu.flags[0] & (1 << 17) ? "VM" : " ";
59+
const pe = cpu.cr[0] & 1 ? "PE" : " ";
60+
return `${ip} ${vm}${pe} cpl${cpu.cpl[0]}`;
61+
} catch { return "?"; }
62+
};
63+
const W = [0x3c0, 0x3c2, 0x3c4, 0x3c5, 0x3ce, 0x3cf, 0x3d4, 0x3d5, 0x3b4, 0x3b5, 0x1ce, 0x1cf];
64+
const R = [0x1cf, 0x3da, 0x3c1];
65+
for (const p of W) for (const w of ["write8", "write16"]) {
66+
const orig = io.ports[p][w];
67+
io.ports[p][w] = function (v: number) {
68+
vgaTrace!.push([p, w, v, ctx()]);
69+
return orig.call(this, v);
70+
};
71+
}
72+
for (const p of R) for (const r of ["read8", "read16"]) {
73+
const orig = io.ports[p][r];
74+
io.ports[p][r] = function () {
75+
const v = orig.call(this);
76+
vgaTrace!.push([p, r, v, ctx()]);
77+
return v;
78+
};
79+
}
80+
console.log("[probe] vga trace armed");
81+
}
82+
83+
function dumpVgaTrace(emulator: any) {
84+
if (!vgaTrace) return;
85+
const d = emulator.v86?.cpu?.devices?.vga;
86+
const state = d && {
87+
svga_enabled: d.svga_enabled,
88+
graphical_mode: d.graphical_mode,
89+
attribute_mode: d.attribute_mode,
90+
miscellaneous_graphics_register: d.miscellaneous_graphics_register,
91+
sequencer_memory_mode: d.sequencer_memory_mode,
92+
clocking_mode: d.clocking_mode,
93+
plane_write_bm: d.plane_write_bm,
94+
crtc_mode: d.crtc_mode,
95+
max_scan_line: d.max_scan_line,
96+
underline_location_register: d.underline_location_register,
97+
horizontal_display_enable_end: d.horizontal_display_enable_end,
98+
horizontal_blank_start: d.horizontal_blank_start,
99+
vertical_display_enable_end: d.vertical_display_enable_end,
100+
vertical_blank_start: d.vertical_blank_start,
101+
offset_register: d.offset_register,
102+
dispi_enable_value: d.dispi_enable_value,
103+
screen_width: d.screen_width,
104+
screen_height: d.screen_height,
105+
max_cols: d.max_cols,
106+
max_rows: d.max_rows,
107+
};
108+
fs.writeFileSync(VGATRACE_FILE, JSON.stringify({ state, trace: vgaTrace }));
109+
}
110+
41111
function sendChord(emu: any, ...keys: { dn: number[]; up: number[] }[]) {
42112
for (const k of keys) emu.keyboard_send_scancodes(k.dn);
43113
setTimeout(() => {
@@ -80,10 +150,16 @@ export function startProbe(emulator: any) {
80150
// Enter, then optional WIN95_PROBE_RUN_AFTER keystrokes after _RUN_WAIT ms.
81151
const runCmd = process.env.WIN95_PROBE_RUN;
82152
const runAfter = process.env.WIN95_PROBE_RUN_AFTER;
83-
let scriptArmed = !!scriptCmd || !!runCmd;
153+
// WIN95_PROBE_DOSBOX=1 → after desktop, open COMMAND.COM, type `dir`,
154+
// optionally Alt+Enter to fullscreen. Regression test for the windowed
155+
// DOS box clobbering VBE (felixrieseberg/v86 vga-defer-vbe-disable-v86).
156+
const dosBox = process.env.WIN95_PROBE_DOSBOX === "1";
157+
const wantVgaTrace = process.env.WIN95_PROBE_VGATRACE === "1";
158+
let scriptArmed = !!scriptCmd || !!runCmd || dosBox;
84159

85160
const tick = () => {
86161
try {
162+
if (wantVgaTrace && !vgaTrace) armVgaTrace(emulator);
87163
const s = collectStatus(emulator);
88164
fs.writeFileSync(STATUS_FILE, JSON.stringify(s, null, 2));
89165

@@ -98,11 +174,46 @@ export function startProbe(emulator: any) {
98174
}
99175
} catch {}
100176

177+
dumpVgaTrace(emulator);
178+
101179
// Once at desktop, fire the keyboard script (once). The 8s settle is
102180
// for the "Welcome to Windows 95" tip dialog to be dismissable —
103181
// we send Esc first to clear it.
104182
if (scriptArmed && s.phase === "desktop" && s.uptimeSec > 8) {
105183
scriptArmed = false;
184+
if (dosBox) {
185+
console.log("[probe] desktop detected, opening DOS box");
186+
runScript(emulator, [
187+
{ type: "wait", ms: 3000 },
188+
{ type: "keys", dn: SC.ESC_DN, up: SC.ESC_UP },
189+
{ type: "wait", ms: 1000 },
190+
{ type: "keys", dn: SC.ESC_DN, up: SC.ESC_UP },
191+
{ type: "wait", ms: 1000 },
192+
{ type: "chord", keys: [
193+
{ dn: SC.CTRL_DN, up: SC.CTRL_UP },
194+
{ dn: SC.ESC_DN, up: SC.ESC_UP },
195+
]},
196+
{ type: "wait", ms: 1200 },
197+
{ type: "keys", dn: SC.R_DN, up: SC.R_UP },
198+
{ type: "wait", ms: 1000 },
199+
{ type: "text", text: "command" },
200+
{ type: "wait", ms: 400 },
201+
{ type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP },
202+
{ type: "wait", ms: 5000 },
203+
{ type: "text", text: "dir" },
204+
{ type: "wait", ms: 200 },
205+
{ type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP },
206+
{ type: "wait", ms: 3000 },
207+
...(process.env.WIN95_PROBE_DOSBOX_ALTENTER === "1" ? [
208+
{ type: "chord", keys: [
209+
{ dn: SC.ALT_DN, up: SC.ALT_UP },
210+
{ dn: SC.ENTER_DN, up: SC.ENTER_UP },
211+
]},
212+
{ type: "wait", ms: 4000 },
213+
] : []),
214+
]);
215+
return;
216+
}
106217
if (runCmd) {
107218
console.log("[probe] desktop detected, Run →", runCmd);
108219
runScript(emulator, [
@@ -128,7 +239,8 @@ export function startProbe(emulator: any) {
128239
{ type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP },
129240
] : []),
130241
]);
131-
} else {
242+
return;
243+
}
132244
console.log("[probe] desktop detected, running script:", scriptCmd);
133245
runScript(emulator, [
134246
{ type: "wait", ms: 3000 },
@@ -160,7 +272,6 @@ export function startProbe(emulator: any) {
160272
{ type: "wait", ms: 400 },
161273
{ type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP },
162274
]);
163-
}
164275
}
165276

166277
if (s.verdict) {
@@ -273,7 +384,7 @@ function collectStatus(emulator: any): ProbeStatus {
273384
// Made it to ≥640×480 graphics → desktop reached. But if a keyboard
274385
// script is running, hold off — the outer harness reads the SMB log
275386
// directly and we just keep the app alive.
276-
else if (atDesktop && uptimeSec > 30 && !process.env.WIN95_PROBE_SCRIPT && !process.env.WIN95_PROBE_RUN) verdict = "SUCCESS";
387+
else if (atDesktop && uptimeSec > 30 && !process.env.WIN95_PROBE_SCRIPT && !process.env.WIN95_PROBE_RUN && !process.env.WIN95_PROBE_DOSBOX) verdict = "SUCCESS";
277388
// Timeout
278389
else if (uptimeSec > 180) verdict = "FAIL_OTHER";
279390

0 commit comments

Comments
 (0)