diff --git a/.claude/skills/probe-win95/SKILL.md b/.claude/skills/probe-win95/SKILL.md index 1a8c71a..9d433ee 100644 --- a/.claude/skills/probe-win95/SKILL.md +++ b/.claude/skills/probe-win95/SKILL.md @@ -37,7 +37,13 @@ WIN95_SMB_SHARE="$HOME/Downloads" \ ``` `WIN95_PROBE_SCRIPT='HOST/HOST'` types `\\HOST\HOST` into Start → Run on -desktop. `/` → `\` substitution (env var / shell quoting, pragmatism). The +desktop. `WIN95_PROBE_DOSBOX=1` instead opens `command`, types `dir`, +and (with `WIN95_PROBE_DOSBOX_ALTENTER=1`) toggles fullscreen — this is +the regression scenario for the windowed-DOS-box VBE leak. +`WIN95_PROBE_VGATRACE=1` wraps the VGA I/O ports at the `io.ports[]` +layer and writes `[port, op, value, "eip VMPE cplN"]` tuples to +`/tmp/win95-vgatrace.json` every tick (heavy — can hit 1M entries during +boot). `/` → `\` substitution (env var / shell quoting, pragmatism). The harness drives it via XT scancodes — Win95 doesn't have Win+R (Win98+ only), so the sequence is Esc, Esc, Ctrl+Esc, R, backslashes + text, Enter. diff --git a/.claude/skills/update-v86/SKILL.md b/.claude/skills/update-v86/SKILL.md index 95bcfcc..32ff1eb 100644 --- a/.claude/skills/update-v86/SKILL.md +++ b/.claude/skills/update-v86/SKILL.md @@ -23,7 +23,7 @@ fallbacks, no fetching from copy.sh. ## The fork branch v86 should be checked out on **`felixrieseberg/v86:windows95-base`**. -That branch merges three feature branches, each upstreamable on its own: +That branch merges four feature branches, each upstreamable on its own: - **`electron-renderer-fs-loader`** (PR #1540) — `src/lib.js` uses `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: (VBADOS VBMOUSE) can read absolute cursor position and track the host cursor 1:1 without pointer lock. Consumes the `mouse-absolute` bus event that `MouseAdapter` already emits. +- **`vga-defer-vbe-disable-v86`** — `src/vga.js` defers `dispi[4]=0` + written from V86 mode until a legacy attribute-mode write reaches the + hardware. Win9x's VDD virtualises ports 3B0–3DF for a windowed DOS VM + but not 1CE/1CF, so vgabios's VBE-disable leaks through while the rest + of its mode-set is captured into the VM's virtual register file — + without this the screen turns to planar garbage the moment you open a + DOS box. ## Prerequisites diff --git a/src/renderer/debug-harness.ts b/src/renderer/debug-harness.ts index 1e8199a..6c3051c 100644 --- a/src/renderer/debug-harness.ts +++ b/src/renderer/debug-harness.ts @@ -36,8 +36,78 @@ const SC = { R_DN: [0x13], R_UP: [0x93], ENTER_DN: [0x1c], ENTER_UP: [0x9c], BACKSLASH_DN: [0x2b], BACKSLASH_UP: [0xab], + ALT_DN: [0x38], ALT_UP: [0xb8], }; +// WIN95_PROBE_VGATRACE=1 → wrap VGA I/O ports at the io.ports[] layer (the +// VGAScreen.portXXX_write methods are captured by-value at registration time, +// so monkey-patching them on the instance is a no-op for most ports). Each +// entry is [port, op, value, "eip VMPE cplN"] so you can tell vgabios in V86 +// mode apart from the ring-0 display driver. +const VGATRACE_FILE = "/tmp/win95-vgatrace.json"; +let vgaTrace: any[] | undefined; + +function armVgaTrace(emulator: any) { + const cpu = emulator.v86?.cpu; + const io = cpu?.io; + if (!io || vgaTrace) return; + vgaTrace = []; + const ctx = () => { + try { + const ip = (cpu.instruction_pointer[0] >>> 0).toString(16); + const vm = cpu.flags[0] & (1 << 17) ? "VM" : " "; + const pe = cpu.cr[0] & 1 ? "PE" : " "; + return `${ip} ${vm}${pe} cpl${cpu.cpl[0]}`; + } catch { return "?"; } + }; + const W = [0x3c0, 0x3c2, 0x3c4, 0x3c5, 0x3ce, 0x3cf, 0x3d4, 0x3d5, 0x3b4, 0x3b5, 0x1ce, 0x1cf]; + const R = [0x1cf, 0x3da, 0x3c1]; + for (const p of W) for (const w of ["write8", "write16"]) { + const orig = io.ports[p][w]; + io.ports[p][w] = function (v: number) { + vgaTrace!.push([p, w, v, ctx()]); + return orig.call(this, v); + }; + } + for (const p of R) for (const r of ["read8", "read16"]) { + const orig = io.ports[p][r]; + io.ports[p][r] = function () { + const v = orig.call(this); + vgaTrace!.push([p, r, v, ctx()]); + return v; + }; + } + console.log("[probe] vga trace armed"); +} + +function dumpVgaTrace(emulator: any) { + if (!vgaTrace) return; + const d = emulator.v86?.cpu?.devices?.vga; + const state = d && { + svga_enabled: d.svga_enabled, + graphical_mode: d.graphical_mode, + attribute_mode: d.attribute_mode, + miscellaneous_graphics_register: d.miscellaneous_graphics_register, + sequencer_memory_mode: d.sequencer_memory_mode, + clocking_mode: d.clocking_mode, + plane_write_bm: d.plane_write_bm, + crtc_mode: d.crtc_mode, + max_scan_line: d.max_scan_line, + underline_location_register: d.underline_location_register, + horizontal_display_enable_end: d.horizontal_display_enable_end, + horizontal_blank_start: d.horizontal_blank_start, + vertical_display_enable_end: d.vertical_display_enable_end, + vertical_blank_start: d.vertical_blank_start, + offset_register: d.offset_register, + dispi_enable_value: d.dispi_enable_value, + screen_width: d.screen_width, + screen_height: d.screen_height, + max_cols: d.max_cols, + max_rows: d.max_rows, + }; + fs.writeFileSync(VGATRACE_FILE, JSON.stringify({ state, trace: vgaTrace })); +} + function sendChord(emu: any, ...keys: { dn: number[]; up: number[] }[]) { for (const k of keys) emu.keyboard_send_scancodes(k.dn); setTimeout(() => { @@ -80,10 +150,16 @@ export function startProbe(emulator: any) { // Enter, then optional WIN95_PROBE_RUN_AFTER keystrokes after _RUN_WAIT ms. const runCmd = process.env.WIN95_PROBE_RUN; const runAfter = process.env.WIN95_PROBE_RUN_AFTER; - let scriptArmed = !!scriptCmd || !!runCmd; + // WIN95_PROBE_DOSBOX=1 → after desktop, open COMMAND.COM, type `dir`, + // optionally Alt+Enter to fullscreen. Regression test for the windowed + // DOS box clobbering VBE (felixrieseberg/v86 vga-defer-vbe-disable-v86). + const dosBox = process.env.WIN95_PROBE_DOSBOX === "1"; + const wantVgaTrace = process.env.WIN95_PROBE_VGATRACE === "1"; + let scriptArmed = !!scriptCmd || !!runCmd || dosBox; const tick = () => { try { + if (wantVgaTrace && !vgaTrace) armVgaTrace(emulator); const s = collectStatus(emulator); fs.writeFileSync(STATUS_FILE, JSON.stringify(s, null, 2)); @@ -98,11 +174,46 @@ export function startProbe(emulator: any) { } } catch {} + dumpVgaTrace(emulator); + // Once at desktop, fire the keyboard script (once). The 8s settle is // for the "Welcome to Windows 95" tip dialog to be dismissable — // we send Esc first to clear it. if (scriptArmed && s.phase === "desktop" && s.uptimeSec > 8) { scriptArmed = false; + if (dosBox) { + console.log("[probe] desktop detected, opening DOS box"); + runScript(emulator, [ + { type: "wait", ms: 3000 }, + { type: "keys", dn: SC.ESC_DN, up: SC.ESC_UP }, + { type: "wait", ms: 1000 }, + { type: "keys", dn: SC.ESC_DN, up: SC.ESC_UP }, + { type: "wait", ms: 1000 }, + { type: "chord", keys: [ + { dn: SC.CTRL_DN, up: SC.CTRL_UP }, + { dn: SC.ESC_DN, up: SC.ESC_UP }, + ]}, + { type: "wait", ms: 1200 }, + { type: "keys", dn: SC.R_DN, up: SC.R_UP }, + { type: "wait", ms: 1000 }, + { type: "text", text: "command" }, + { type: "wait", ms: 400 }, + { type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP }, + { type: "wait", ms: 5000 }, + { type: "text", text: "dir" }, + { type: "wait", ms: 200 }, + { type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP }, + { type: "wait", ms: 3000 }, + ...(process.env.WIN95_PROBE_DOSBOX_ALTENTER === "1" ? [ + { type: "chord", keys: [ + { dn: SC.ALT_DN, up: SC.ALT_UP }, + { dn: SC.ENTER_DN, up: SC.ENTER_UP }, + ]}, + { type: "wait", ms: 4000 }, + ] : []), + ]); + return; + } if (runCmd) { console.log("[probe] desktop detected, Run →", runCmd); runScript(emulator, [ @@ -128,7 +239,8 @@ export function startProbe(emulator: any) { { type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP }, ] : []), ]); - } else { + return; + } console.log("[probe] desktop detected, running script:", scriptCmd); runScript(emulator, [ { type: "wait", ms: 3000 }, @@ -160,7 +272,6 @@ export function startProbe(emulator: any) { { type: "wait", ms: 400 }, { type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP }, ]); - } } if (s.verdict) { @@ -273,7 +384,7 @@ function collectStatus(emulator: any): ProbeStatus { // Made it to ≥640×480 graphics → desktop reached. But if a keyboard // script is running, hold off — the outer harness reads the SMB log // directly and we just keep the app alive. - else if (atDesktop && uptimeSec > 30 && !process.env.WIN95_PROBE_SCRIPT && !process.env.WIN95_PROBE_RUN) verdict = "SUCCESS"; + else if (atDesktop && uptimeSec > 30 && !process.env.WIN95_PROBE_SCRIPT && !process.env.WIN95_PROBE_RUN && !process.env.WIN95_PROBE_DOSBOX) verdict = "SUCCESS"; // Timeout else if (uptimeSec > 180) verdict = "FAIL_OTHER"; diff --git a/src/renderer/lib/libv86.js b/src/renderer/lib/libv86.js index fbf1008..c76c6f6 100644 --- a/src/renderer/lib/libv86.js +++ b/src/renderer/lib/libv86.js @@ -426,11 +426,13 @@ this.mouse_clicks=this.mouse_delta_x=this.mouse_delta_y=0;break;case 245:this.en 0;break;default:y(a)}this.mouse_irq()}}else if(this.read_controller_output_port)this.read_controller_output_port=!1,this.controller_output_port=a;else{y(a);this.mouse_buffer.clear();this.kbd_buffer.clear();this.kbd_buffer.push(250);switch(a){case 237:this.next_read_led=!0;break;case 240:this.next_handle_scan_code_set=!0;break;case 242:this.kbd_buffer.push(171);this.kbd_buffer.push(131);break;case 243:this.next_read_rate=!0;break;case 244:this.enable_keyboard_stream=!0;break;case 245:this.enable_keyboard_stream= !1;break;case 246:break;case 255:this.kbd_buffer.clear();this.kbd_buffer.push(250);this.kbd_buffer.push(170);this.kbd_buffer.push(0);break;default:y(a)}this.kbd_irq()}}; Gc.prototype.port64_write=function(a){y(a);switch(a){case 32:this.kbd_buffer.clear();this.mouse_buffer.clear();this.kbd_buffer.push(this.command_register);this.kbd_irq();break;case 96:this.read_command_register=!0;break;case 209:this.read_controller_output_port=!0;break;case 211:this.read_output_register=!0;break;case 212:this.next_is_mouse_command=!0;break;case 167:this.command_register|=32;break;case 168:this.command_register&=-33;break;case 169:this.kbd_buffer.clear();this.mouse_buffer.clear(); -this.kbd_buffer.push(0);this.kbd_irq();break;case 170:this.kbd_buffer.clear();this.mouse_buffer.clear();this.kbd_buffer.push(85);this.kbd_irq();break;case 171:this.kbd_buffer.clear();this.mouse_buffer.clear();this.kbd_buffer.push(0);this.kbd_irq();break;case 173:this.command_register|=16;break;case 174:this.command_register&=-17;break;case 254:this.cpu.reboot_internal();break;default:y(a)}};function Hc(a,b){this.cpu=a;this.bus=b;this.absolute=this.enabled=!1;this.queue=[];this.buttons=0;this.last_y=this.last_x=-1;this.tail_is_move=!1;this.bus.register("mouse-absolute",function(c){const d=Math.max(0,Math.min(65535,Math.round(c[0]/c[2]*65535)));c=Math.max(0,Math.min(65535,Math.round(c[1]/c[3]*65535)));if(d!==this.last_x||c!==this.last_y)this.last_x=d,this.last_y=c,this.push_packet(0,!0)},this);this.bus.register("mouse-click",function(c){this.buttons=(c[0]?32:0)|(c[1]?8:0)|(c[2]?16:0); -this.push_packet(0,!1)},this);this.bus.register("mouse-wheel",function(c){this.push_packet(-c[0]|0,!1)},this);a.io.register_read(22104,this,void 0,void 0,this.port_read32);a.io.register_write(22104,this,void 0,void 0,this.port_write32)} -Hc.prototype.push_packet=function(a,b){!this.enabled||!this.absolute||0>this.last_x||(b&&this.tail_is_move&&4<=this.queue.length?(this.queue[this.queue.length-3]=this.last_x,this.queue[this.queue.length-2]=this.last_y):1024>>0,4,this.queue.length),c=[0,0,0,0];for(let d=0;d-1}}(7)},{type:Kc},{machine:Kc},{version1:U},{entry:U},{phoff:U},{shoff:U},{flags:U},{ehsize:Kc},{phentsize:Kc},{phnum:Kc},{shentsize:Kc},{shnum:Kc},{shstrndx:Kc}]);console.assert(52===Mc.reduce((a,b)=>a+b.size,0)); +this.kbd_buffer.push(0);this.kbd_irq();break;case 170:this.kbd_buffer.clear();this.mouse_buffer.clear();this.kbd_buffer.push(85);this.kbd_irq();break;case 171:this.kbd_buffer.clear();this.mouse_buffer.clear();this.kbd_buffer.push(0);this.kbd_irq();break;case 173:this.command_register|=16;break;case 174:this.command_register&=-17;break;case 254:this.cpu.reboot_internal();break;default:y(a)}};function Hc(a,b){this.cpu=a;this.bus=b;this.absolute=this.enabled=!1;this.queue=[];this.buttons=0;this.last_y=this.last_x=-1;this.tail_is_move=!1;this.clip_out=new Uint8Array(0);this.clip_out_cursor=0;this.clip_out_fresh=!1;this.clip_in=new Uint8Array(0);this.clip_in_cursor=0;this.bus.register("vmware-clipboard-host",function(c){this.clip_out=65536this.last_x||(b&&this.tail_is_move&&4<=this.queue.length?(this.queue[this.queue.length-3]=this.last_x,this.queue[this.queue.length-2]=this.last_y):1024>>0,65536),this.clip_in=new Uint8Array(a),this.clip_in_cursor=0,0===a&&this.bus.send("vmware-clipboard-guest", +this.clip_in),0;case 9:b=this.clip_in;a=a[3]>>>0;var c=this.clip_in_cursor;c>>8);c>>16);c>>24);this.clip_in_cursor=c;c>=b.length&&(this.bus.send("vmware-clipboard-guest",b),this.clip_in=new Uint8Array(0),this.clip_in_cursor=0);return 0;case 40:return this.enabled?this.queue.length:-65536;case 39:b=Math.min(a[3]>>>0,4,this.queue.length);c=[0,0,0,0];for(let d=0;d-1}}(7)},{type:Kc},{machine:Kc},{version1:U},{entry:U},{phoff:U},{shoff:U},{flags:U},{ehsize:Kc},{phentsize:Kc},{phnum:Kc},{shentsize:Kc},{shnum:Kc},{shstrndx:Kc}]);console.assert(52===Mc.reduce((a,b)=>a+b.size,0)); const Nc=Lc([{type:U},{offset:U},{vaddr:U},{paddr:U},{filesz:U},{memsz:U},{flags:U},{align:U}]);console.assert(32===Nc.reduce((a,b)=>a+b.size,0));const Oc=Lc([{name:U},{type:U},{flags:U},{addr:U},{offset:U},{size:U},{link:U},{info:U},{addralign:U},{entsize:U}]);console.assert(40===Oc.reduce((a,b)=>a+b.size,0));function Lc(a){return a.map(function(b){var c=Object.keys(b);console.assert(1===c.length);c=c[0];b=b[c];console.assert(0this.attribute_controller_index)y(this.attribute_controller_index),y(a),this.dac_map[this.attribute_controller_index]=a,this.attribute_mode&64||this.complete_redraw();else switch(this.attribute_controller_index){case 16:y(a);if(this.attribute_mode!==a){var b= -this.attribute_mode;this.attribute_mode=a;const c=0!==(a&1);this.svga_enabled||this.graphical_mode===c||(this.graphical_mode=c,this.screen.set_mode(this.graphical_mode));(b^a)&64&&this.complete_replot();this.update_vga_size();this.complete_redraw();this.set_font_bitmap(!1)}break;case 18:y(a);this.color_plane_enable!==a&&(this.color_plane_enable=a,this.complete_redraw());break;case 19:y(a);this.horizontal_panning!==a&&(this.horizontal_panning=a&15,this.update_layers());break;case 20:y(a);this.color_select!== -a&&(this.color_select=a,this.complete_redraw());break;default:y(this.attribute_controller_index),y(a)}this.attribute_controller_index=-1}};X.prototype.port3C0_read=function(){return(this.attribute_controller_index|this.palette_source)&255};X.prototype.port3C0_read16=function(){return this.port3C0_read()|this.port3C1_read()<<8&65280}; +this.attribute_mode;this.attribute_mode=a;!this.svga_enabled||this.dispi_enable_value&1||(this.svga_enabled=!1,this.svga_bank_offset=0);const c=0!==(a&1);this.svga_enabled||this.graphical_mode===c||(this.graphical_mode=c,this.screen.set_mode(this.graphical_mode));(b^a)&64&&this.complete_replot();this.update_vga_size();this.complete_redraw();this.set_font_bitmap(!1)}break;case 18:y(a);this.color_plane_enable!==a&&(this.color_plane_enable=a,this.complete_redraw());break;case 19:y(a);this.horizontal_panning!== +a&&(this.horizontal_panning=a&15,this.update_layers());break;case 20:y(a);this.color_select!==a&&(this.color_select=a,this.complete_redraw());break;default:y(this.attribute_controller_index),y(a)}this.attribute_controller_index=-1}};X.prototype.port3C0_read=function(){return(this.attribute_controller_index|this.palette_source)&255};X.prototype.port3C0_read16=function(){return this.port3C0_read()|this.port3C1_read()<<8&65280}; X.prototype.port3C1_read=function(){if(16>this.attribute_controller_index)return y(this.attribute_controller_index),y(this.dac_map[this.attribute_controller_index]),this.dac_map[this.attribute_controller_index]&255;switch(this.attribute_controller_index){case 16:return y(this.attribute_mode),this.attribute_mode;case 18:return y(this.color_plane_enable),this.color_plane_enable;case 19:return y(this.horizontal_panning),this.horizontal_panning;case 20:return y(this.color_select),this.color_select;default:y(this.attribute_controller_index)}return 255}; X.prototype.port3C2_write=function(a){y(a);this.miscellaneous_output_register=a};X.prototype.port3C4_write=function(a){this.sequencer_index=a};X.prototype.port3C4_read=function(){return this.sequencer_index}; X.prototype.port3C5_write=function(a){switch(this.sequencer_index){case 1:y(a);var b=this.clocking_mode;this.clocking_mode=a;(b^a)&32&&this.update_layers();this.set_font_bitmap(!1);break;case 2:y(a);b=this.plane_write_bm;this.plane_write_bm=a;this.graphical_mode||!(b&4)||this.plane_write_bm&4||this.set_font_bitmap(!0);break;case 3:y(a);b=this.character_map_select;this.character_map_select=a;this.graphical_mode||b===a||this.set_font_page();break;case 4:y(a);this.sequencer_memory_mode=a;break;default:y(this.sequencer_index), @@ -608,9 +610,10 @@ a,this.update_vga_size(),(b^a)&67&&this.complete_replot());break;case 24:y(a);th X.prototype.port3D5_read=function(){y(this.index_crtc);switch(this.index_crtc){case 1:return this.horizontal_display_enable_end;case 2:return this.horizontal_blank_start;case 7:return this.vertical_display_enable_end>>7&2|this.vertical_blank_start>>5&8|this.line_compare>>4&16|this.vertical_display_enable_end>>3&64;case 8:return this.preset_row_scan;case 9:return this.max_scan_line;case 10:return this.cursor_scanline_start;case 11:return this.cursor_scanline_end;case 12:return this.start_address&255; case 13:return this.start_address>>8;case 14:return this.cursor_address>>8;case 15:return this.cursor_address&255;case 18:return this.vertical_display_enable_end&255;case 19:return this.offset_register;case 20:return this.underline_location_register;case 21:return this.vertical_blank_start&255;case 23:return this.crtc_mode;case 24:return this.line_compare&255}return this.index_crtc=a?this.svga_version=a:y(a);break;case 1:this.svga_width=a;2560=a?this.svga_version=a:y(a);break;case 1:this.svga_width=a;2560>>16;case 6:return this.screen_width?this.screen_width:1;case 8:return this.svga_offset_x;case 9:return this.svga_offset_y;case 10:return this.vga_memory_size/65536| 0;default:y(this.dispi_index)}return 255}; X.prototype.vga_replot=function(){for(var a=this.diff_plot_min&-16,b=Math.min(this.diff_plot_max|15,524287),c=this.vga_addr_shift_count(),d=~this.crtc_mode&3,e=this.planar_mode&96,f=this.attribute_mode&64;a<=b;){var g=a>>>c;if(d){var h=a/this.virtual_width|0,l=a-this.virtual_width*h;switch(d){case 1:g=(h&1)<<13;h>>>=1;break;case 2:g=(h&1)<<14;h>>>=1;break;case 3:g=(h&3)<<13,h>>>=2}g|=(h*this.virtual_width+l>>>c)+this.start_address}h=this.plane0[g];l=this.plane1[g];var m=this.plane2[g],n=this.plane3[g];