mirror of
https://github.com/felixrieseberg/windows95.git
synced 2026-05-09 00:24:09 +00:00
Enable CD-ROM via a synchronous fs-backed buffer (#362)
v86's async loaders leave the ATAPI drive in BSY across an event-loop turn after a READ(10) CDB. Win95's ESDI_506 reads status twice, sees BSY both times, and issues DEVICE RESET ~165 instructions later, which cancels the in-flight read — the drive enumerates but D: never mounts. Serve the ISO through a small fs.readSync-backed buffer so the data is available before the next emulated instruction runs, and re-enable the CD-ROM settings tab. Also: WIN95_PROBE_CDROM / WIN95_PROBE_CDTRACE harness hooks, and pump one screen-adapter frame before screenshotting so probe captures work when the Electron window is occluded.
This commit is contained in:
@@ -40,6 +40,9 @@ WIN95_SMB_SHARE="$HOME/Downloads" \
|
||||
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_CDROM=/path/to.iso` mounts an ISO on the secondary-IDE
|
||||
ATAPI drive (bypasses the settings UI). `WIN95_PROBE_CDTRACE=1` logs
|
||||
every secondary-channel ATA/ATAPI command to `/tmp/win95-cdtrace.log`.
|
||||
`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
|
||||
|
||||
@@ -3,8 +3,7 @@ import * as React from "react";
|
||||
import { resetState } from "./utils/reset-state";
|
||||
import { InfoBarSettings } from "./info-bar-settings";
|
||||
|
||||
// v86's IDE CD-ROM path is currently broken; flip this once it works again.
|
||||
const CDROM_ENABLED = false;
|
||||
const CDROM_ENABLED = true;
|
||||
|
||||
interface CardSettingsProps {
|
||||
bootFromScratch: () => void;
|
||||
|
||||
@@ -39,6 +39,41 @@ const SC = {
|
||||
ALT_DN: [0x38], ALT_UP: [0xb8],
|
||||
};
|
||||
|
||||
// WIN95_PROBE_CDTRACE=1 → wrap secondary-IDE ata_command/atapi_handle and
|
||||
// log every command so we can see whether Win95's ESDI_506/CDVSD stack ever
|
||||
// talks to the drive (and which ATAPI CDBs it sends).
|
||||
const CDTRACE_FILE = process.env.WIN95_PROBE_CDTRACE_FILE || "/tmp/win95-cdtrace.log";
|
||||
let cdTraceArmed = false;
|
||||
|
||||
function armCdTrace(emulator: any) {
|
||||
const dev = emulator.v86?.cpu?.devices;
|
||||
if (!dev || cdTraceArmed) return;
|
||||
cdTraceArmed = true;
|
||||
const sec = dev.ide?.secondary;
|
||||
fs.writeFileSync(CDTRACE_FILE,
|
||||
`[probe] cd buffer=${!!dev.cdrom?.buffer} bytes=${dev.cdrom?.buffer?.byteLength} is_atapi=${sec?.master?.is_atapi}\n`);
|
||||
const t0 = Date.now();
|
||||
const log = (s: string) => fs.appendFileSync(CDTRACE_FILE, `[${((Date.now()-t0)/1000).toFixed(2)}s] ${s}\n`);
|
||||
const proto = Object.getPrototypeOf(sec?.master || {});
|
||||
for (const m of ["ata_command", "atapi_handle"]) {
|
||||
const orig = proto?.[m];
|
||||
if (typeof orig !== "function") continue;
|
||||
proto[m] = function (this: any, ...a: any[]) {
|
||||
if (this === sec?.master || this === sec?.slave) {
|
||||
const who = this === sec.master ? "sm" : "ss";
|
||||
if (m === "ata_command") log(`${who} ata cmd=0x${(a[0] ?? 0).toString(16)}`);
|
||||
else {
|
||||
const d = this.data || [];
|
||||
const cdb = Array.from(d.slice?.(0, 12) || []).map((b: any) => b.toString(16).padStart(2, "0")).join(" ");
|
||||
log(`${who} atapi cmd=0x${(d[0] ?? 0).toString(16)} cdb=[${cdb}]`);
|
||||
}
|
||||
}
|
||||
return orig.apply(this, a);
|
||||
};
|
||||
}
|
||||
console.log("[probe] cd trace armed");
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -155,17 +190,22 @@ export function startProbe(emulator: any) {
|
||||
// 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";
|
||||
const wantCdTrace = process.env.WIN95_PROBE_CDTRACE === "1";
|
||||
let scriptArmed = !!scriptCmd || !!runCmd || dosBox;
|
||||
|
||||
const tick = () => {
|
||||
try {
|
||||
if (wantVgaTrace && !vgaTrace) armVgaTrace(emulator);
|
||||
if (wantCdTrace && !cdTraceArmed) armCdTrace(emulator);
|
||||
const s = collectStatus(emulator);
|
||||
fs.writeFileSync(STATUS_FILE, JSON.stringify(s, null, 2));
|
||||
|
||||
// Try to capture a screenshot — this can fail if the screen adapter
|
||||
// isn't ready yet, so we swallow that.
|
||||
try {
|
||||
// rAF doesn't fire when the Electron window is occluded, so the
|
||||
// screen adapter's render loop stalls. Pump one frame by hand.
|
||||
try { emulator.screen_adapter?.update_screen?.(); } catch {}
|
||||
const img: HTMLImageElement = emulator.screen_make_screenshot();
|
||||
// The Image has a data: URL src; decode it to bytes
|
||||
if (img && img.src && img.src.startsWith("data:image/png;base64,")) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { setupSmbShare } from "./smb";
|
||||
import { setupTcpRelay } from "./net/tcp-relay";
|
||||
import { setupDnsShim } from "./net/dns-shim";
|
||||
import { startProbe } from "./debug-harness";
|
||||
import { SyncFileBuffer } from "./sync-file-buffer";
|
||||
|
||||
const PROBE = process.env.WIN95_PROBE === "1";
|
||||
const PROBE_OPTS: Record<string, unknown> = (() => {
|
||||
@@ -336,9 +337,11 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
private async startEmulator() {
|
||||
document.body.classList.remove("paused");
|
||||
|
||||
const cdromPath = this.state.cdromFile
|
||||
? webUtils.getPathForFile(this.state.cdromFile)
|
||||
: null;
|
||||
const cdromPath =
|
||||
process.env.WIN95_PROBE_CDROM ||
|
||||
(this.state.cdromFile
|
||||
? webUtils.getPathForFile(this.state.cdromFile)
|
||||
: null);
|
||||
|
||||
const options = {
|
||||
wasm_path: path.join(__dirname, "build/v86.wasm"),
|
||||
@@ -372,13 +375,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
buffer: this.state.floppyFile,
|
||||
}
|
||||
: undefined,
|
||||
cdrom: cdromPath
|
||||
? {
|
||||
url: cdromPath,
|
||||
async: true,
|
||||
size: await getDiskImageSize(cdromPath),
|
||||
}
|
||||
: undefined,
|
||||
cdrom: cdromPath ? new SyncFileBuffer(cdromPath) : undefined,
|
||||
boot_order: 0x132,
|
||||
};
|
||||
|
||||
|
||||
50
src/renderer/sync-file-buffer.ts
Normal file
50
src/renderer/sync-file-buffer.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as fs from "fs";
|
||||
|
||||
/**
|
||||
* v86 disk buffer backed by synchronous fs reads.
|
||||
*
|
||||
* v86's stock async loaders (AsyncXHRBuffer / AsyncFileBuffer) return from
|
||||
* .get() immediately and resolve the data on a later event-loop turn. For an
|
||||
* ATAPI PIO READ(10) that means atapi_read() leaves the drive in BSY while the
|
||||
* emulated CPU keeps running. Win95's ESDI_506/CDVSD path checks status twice
|
||||
* after pushing the CDB, sees BSY both times, and issues DEVICE RESET (08h) —
|
||||
* which cancels the in-flight read. Net effect: D: shows up but the volume
|
||||
* never mounts. Serving the bytes synchronously closes that window.
|
||||
*
|
||||
* The hard disk doesn't hit this because ESDI_506 drives it via bus-master
|
||||
* DMA, which is purely IRQ-driven on the host side.
|
||||
*/
|
||||
export class SyncFileBuffer {
|
||||
public byteLength: number;
|
||||
public onload: undefined | ((e: { buffer?: ArrayBuffer }) => void);
|
||||
public onprogress: undefined | (() => void);
|
||||
|
||||
private fd: number;
|
||||
|
||||
constructor(path: string) {
|
||||
this.fd = fs.openSync(path, "r");
|
||||
this.byteLength = fs.fstatSync(this.fd).size;
|
||||
this.onload = undefined;
|
||||
this.onprogress = undefined;
|
||||
}
|
||||
|
||||
load() {
|
||||
this.onload?.({});
|
||||
}
|
||||
|
||||
get(start: number, len: number, fn: (data: Uint8Array) => void) {
|
||||
const buf = Buffer.alloc(len);
|
||||
fs.readSync(this.fd, buf, 0, len, start);
|
||||
fn(new Uint8Array(buf.buffer, buf.byteOffset, len));
|
||||
}
|
||||
|
||||
set(_start: number, _slice: Uint8Array, fn: () => void) {
|
||||
fn();
|
||||
}
|
||||
|
||||
get_state() {
|
||||
return [[]];
|
||||
}
|
||||
|
||||
set_state(_state: unknown) {}
|
||||
}
|
||||
Reference in New Issue
Block a user