Files
windows95/.claude/skills/update-v86/SKILL.md
Felix Rieseberg b74e6c7b0a Fix guest TCP recv() stalling when v86 NE2000 TX ring wraps (#363)
* Fix guest TCP recv() stalling under concurrent traffic

v86's fake_network stores TCPConnection routing fields (hsrc/hdest/
psrc/pdest) as zero-copy subarrays of the SYN frame, which is itself a
view into the NE2000 TX ring. Win95's driver uses a 12-slot ring; once
it wraps (any concurrent SMB/NBNS/ping while waiting for an upstream
reply), pump() emits segments with whatever IP now occupies that slot,
the guest RSTs them, and recv() blocks forever.

- libv86.js: copy the four address arrays at TCPConnection construction
  (matches felixrieseberg/v86@dd13099c on fake-network-copy-tcp-addrs,
  now merged into windows95-base)
- tools/probe-tcp.sh + net/tcp-trace.ts + tcp-relay.ts test stub +
  debug-harness WIN95_PROBE_RUN2: end-to-end regression harness
  (boot → ping -t → telnet → async write after ring wrap → assert ACK).
  All env-gated, no production-path change.
- docs/v86-patches.md: tracker for all fork patches + upstream PR state
- update-v86 SKILL.md: cross-link and new fork-branch entry

* Drop checked-in upstream PR description

Belongs on the GitHub PR, not in the repo.
2026-04-12 08:03:03 -07:00

6.4 KiB
Raw Blame History

name, description
name description
update-v86 Build and install v86 (wasm + libv86.js + BIOS) into windows95. Use when pulling upstream v86 changes, fixing a broken build, verifying the fork branches are still in sync, or setting up a fresh v86 checkout.

Updating v86

windows95 builds v86 from source — not from copy.sh. Two small bugfix patches ride along on a fork branch until the upstream PRs land.

Sources

File Built from
src/renderer/lib/libv86.js make build/libv86.js in ../v86
src/renderer/lib/build/v86.wasm make build/v86.wasm
bios/seabios.bin, bios/vgabios.bin copied from ../v86/bios/

tools/update-v86.js runs those targets, copies the artifacts, runs 5 sanity checks, and fails loudly if any prerequisite is missing. No fallbacks, no fetching from copy.sh.

The fork branch

v86 should be checked out on felixrieseberg/v86:windows95-base. That branch merges the feature branches tracked in docs/v86-patches.md — keep that table in sync with this list. Each is upstreamable on its own:

  • electron-renderer-fs-loader (PR #1540) — src/lib.js uses require("fs") instead of await import("node:fs/promises"). Dynamic import of node: URLs doesn't work in an Electron renderer.
  • ide-shared-registers (PR #1541) — src/ide.js writes ATA Command Block registers (Features, Sector Count, LBA Low/Mid/High) to both master and slave. Without this, Win95/98 hang at the splash screen on any disk >~535MiB. Root cause: v86 commit 1b90d2e7 changed those writes to target only current_interface, but per ATA spec they're channel-shared (one register file on the IDE cable; both drives latch the same value).
  • vmware-abspointersrc/vmware.js implements the VMware backdoor (port 0x5658): GETVERSION + ABSPOINTER_* so a guest driver (VBADOS VBMOUSE) can read absolute cursor position and track the host cursor 1:1 without pointer lock, and the legacy text-clipboard commands 69 so W95TOOLS.EXE (guest-tools/agent) can sync CF_TEXT with the host. Consumes mouse-absolute and vmware-clipboard-host bus events; emits vmware-absolute-mouse and vmware-clipboard-guest.
  • fake-network-copy-tcp-addrssrc/browser/fake_network.js copies the four address subarrays (hsrc/hdest/psrc/pdest) when a TCPConnection is created from an inbound SYN. Upstream stores them as zero-copy views into the NE2000 TX ring; once the guest's 12-slot TX ring wraps (any concurrent traffic — SMB, NBNS, ping), pump() builds segments with whatever IP now occupies that slot, the guest RSTs them as belonging to no TCB, and recv() blocks forever. Exercised by tools/probe-tcp.sh.
  • vga-defer-vbe-disable-v86src/vga.js defers dispi[4]=0 written from V86 mode until a legacy attribute-mode write reaches the hardware. Win9x's VDD virtualises ports 3B03DF 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

rustup target add wasm32-unknown-unknown
brew install openjdk
# one-time: fetch the Closure compiler v86's Makefile pins to
curl -sL https://repo1.maven.org/maven2/com/google/javascript/closure-compiler/v20210601/closure-compiler-v20210601.jar \
  -o ../v86/closure-compiler/compiler.jar

Closure must be v20210601 — newer versions hit closure-compiler#3972 on v86's source. The pin is in v86's Makefile.

Steps

cd ../v86
git fetch fork origin
git checkout windows95-base
git rebase fork/windows95-base   # in case fork was updated elsewhere
cd ../windows95
node tools/update-v86.js

That's it. Script runs both make targets, copies, verifies.

Sanity-check WARNs

The 5 checks assert invariants src/renderer/smb/index.ts and tools/parcel-build.js depend on. A WARN means upstream changed something load-bearing — don't ignore it:

  1. await import("node:...") still present → PR #1540 was reverted or the pattern moved. Electron renderer will fail to load disk images.
  2. master.features_reg= missing in minified → PR #1541 was reverted or windows95-base lost the commit. Win95 will hang at splash on disks >535MiB. Check cd ../v86 && git log --oneline windows95-base.
  3. Export pattern changedtools/parcel-build.js shim needs updating. Look for module.exports.V86= and window.V86=.
  4. tcp-connection event gone → SMB falls back to the old-API theft hack in src/renderer/smb/index.ts — still works, but surprising.
  5. on_tcp_connection gone → old-API fallback is dead. SMB integration only works via the tcp-connection bus event now. Harmless; update the comment in index.ts and retire the theft code.

After updating, probe-test

node tools/update-v86.js && tools/probe-boot.sh

Should land SUCCESS in ~40s. If FAIL_SPLASH_HANG, the IDE fix didn't take — check grep master.features_reg src/renderer/lib/libv86.js. If FAIL_VXDLINK, retry — sporadic bluescreens are normal (see the probe-win95 skill).

When a PR merges upstream

Rebase windows95-base to drop the now-redundant commit:

cd ../v86
git fetch origin
git checkout windows95-base
git rebase origin/master              # drops the merged commit cleanly
git push fork windows95-base --force-with-lease

If both PRs are upstream, retire the fork branch entirely:

  1. Point tools/update-v86.js default at origin/master (it already uses ../v86, so just git checkout master there)
  2. Delete fork/windows95-base
  3. Remove this skill's "The fork branch" section
  4. Confirm the 5 sanity checks still pass — they're version-agnostic

Integration contract with SMB

The SMB server sits on top of v86's network adapter. Details in src/renderer/smb/README.md. Short version: the new path uses the tcp-connection bus event; the fallback path uses adapter.on_tcp_connection callback + connection-theft (stealing a TCPConnection the HTTP probe builds for us). Both use .on_data on the conn, not .on("data"), because Closure dead-code-eliminates the event emitter plumbing.

If any v86 update breaks these assumptions, src/renderer/smb/index.ts needs updating, not just tools/update-v86.js.