How Wine-Assembly works: articles, design docs and reverse-engineering notes
Wine-Assembly runs real Windows 98 programs in the browser: an x86 interpreter, a Win32 API layer, a software GDI, DirectX and a 16-bit loader, all written directly in WebAssembly Text. The articles below each answer one question about how that is built and are the place to start; the design docs and per-application reverse-engineering notes after them are the working record the articles are drawn from. The story tells it in order.
Start here
Five pages that cover the whole idea, in the order they build on each other:
- How to write an x86 interpreter in raw WebAssembly Text, the machine everything else runs on.
- Implementing the Win32 API in WebAssembly, the operating system the program thinks it is talking to.
- A software GDI in WebAssembly, how pixels get from the program to the canvas.
- Wine-Assembly vs v86, what this approach gives up and gains against booting a real Windows 98.
- Building an emulator with AI coding agents, how it was actually written.
Articles
The machine
- How to write an x86 interpreter in raw WebAssembly Text
Threaded code throughcall_indirect, the block cache, what a dispatch costs, loop super-ops, and the optimisations that measured as nothing. - Lazy flags: how an x86 emulator avoids computing EFLAGS after every instruction
Four globals instead of a flags register, the bugs the scheme produced, and a second implementation's numbers. - Letting the compiler own the memory map: WATX
Why 170 hand-placed hex bases became untenable, what the extended WAT adds, and how the cutover was verified. - A DOS emulator in WebAssembly built to answer interpreter-design questions
The toy VM: 199 DOS programs, three execution tiers, and what a region JIT found.
The operating system
- Loading real Windows DLLs in the browser
MFC42, msvcrt, d3drm: relocations, ordinal imports, DllMain, and the runtime patches that were not obvious. - Implementing the Win32 API in WebAssembly
3,300 handlers, one generated dispatch table, continuation thunks for calls that call back, and the no-silent-stubs rule. - A software GDI in WebAssembly
Why the canvas 2D API was the wrong target, and the rasteriser, regions, paths and fonts that replaced it. - DirectDraw and Direct3D in the browser
COM vtables through the thunk zone, surfaces as memory, palettes, and three generations of Direct3D. - Running 16-bit Windows 3.x programs in the browser
An NE loader, segmented execution, and Pascal-convention Win16 dispatch. - A virtual LAN for Windows 98 multiplayer games
Winsock in WAT over a frame wire, and what Hearts and Liquid War needed.
The process
- Driving a Windows 98 emulator from a script
Headless runs, frozen mode, the JSON control channel, agent handoff, and bring-your-own media. - Building an emulator with AI coding agents
What 3,700 commits with Claude Code and Codex taught about crashes, tracing, negative results and shared trees.
Compared with other emulators
- Wine-Assembly vs v86
A whole PC that boots a Windows 98 image, against a Win32 layer that runs the program directly. - Wine-Assembly vs Boxedwine
Wine compiled to WebAssembly, against a Windows layer written in WebAssembly Text from scratch. - Wine-Assembly vs DOSBox and js-dos
DOS programs on an emulated PC, against Windows programs with no operating system; where the toy VM fits.
Design docs and reverse-engineering notes
These pages are the working notes behind it — the memory map, the interpreter's dispatch and lazy-flag design, the software GDI rasterizer, the DirectX and Win16 layers, the performance ledger, and one file per game or application we have taken apart to make it run. They are rendered from the Markdown in docs/ on GitHub.
Emulator design and performance
- Bug Fixes — Flag System, Shift CF, and IDIV Overflow
Bug Fixes — Flag System, Shift CF, and IDIV Overflow: notes from the Wine-Assembly Windows 98 emulator. - SIMD Support Design (SSE/MMX)
Transitioning the emulator to support SIMD involves mapping x86 vector registers to WebAssembly v128 types and handling the architectural aliasing between… - Splitting the guest-memory accessors so V8 inlines their fast paths — a null
Measured 2026-08-31 on branch worktree-agent-aefd5647b019c4c9d (a9131389). - AoE Performance Optimization Notes
The bottleneck is the WAT x86 interpreter, not canvas repaint and not mostly DIB conversion. - Win9x bitmap font review
Status: decision implemented for the deterministic GDI stock-font path on 2026-08-14. - Plan: Controls as Real Windows
Currently, dialog controls (buttons, checkboxes, edit fields, static text) are rendered as dumb visual elements by the JS renderer. - Agent control channel: live event streams into a running session
Status: phases 1 and 2 IMPLEMENTED, plus frozen (agent-stepped) mode and the multi-session dashboard — see those two sections below; the phase-2 pause/run… - Bring Your Own Media — design
Status: all six phases landed (4d7497ed provider/lazy-VFS, 6a876ff0 zip, 60407f55 iso + WAT parking, 29dab90a/9c1003b9 overlay, 0995b5a7 save… - Frozen session recording
Record an agent-driven frozen emulator session as a video that plays back as continuous realtime gameplay. Frames and audio are reconstructed on the - Design: real OS threads for guest threads
worktree-real-threads. Written 2026-08-16, updated 2026-08-20. - Production desktop idle CPU audit
Goal: all non-realtime games on the production desktop should use little CPU while idle. Local acceptance complete; merged, not deployed. - Direct3D software and WebGL implementation plan
Status: implementation started, 2026-09-10; neither backend is complete. See the implementation ledger for evidence and open delivery gates. - Dual Direct3D implementation ledger
Reset presentation follow-up48855 PASS: native56-byte parameter extent guard, one-buffer DISCARD/COPY selection, explicit rejection of FLIP/multiple… - DirectAnimation support — design notes
Status: design only, not implemented. Captures what we learned while investigating CORBIS/FASHION/HORROR/WOTRAVEL screensavers (Plus!98) so a future… - DOS corpus: what is still not drawing, and why
The 199-program demo corpus in /tmp/demos is swept with - Frame-pacing census: can we cap guest frame production at display refresh?
Measured 2026-08-31. Tools written for this survey: tools/pacing-census.js, tools/lock-pause-ab.js, and one flag, test/run.js --dx-lock-pause-ms=N. - GDI migration status
The machine-readable source is gdi-migration-status.json. It contains the exact keep/delete API lists for the JavaScript GDI bridge. - Packed guest-page translation experiment
Wine Assembly's common image-relative and DIB guest addresses already use constant-time arithmetic. - Integer expression-region benchmark
tools/int-expr-region-bench.js — the integer twin of the x87 region bench (tools/x87-realistic-region-bench.js / docs/x87-realistic-region-bench.md,… - How much of a DOS demo is an expression?
tools/toyvm/expr-fold-census.js, core ten + three, 20M dispatches each. - Integer expression fusion: is there a ceiling worth building for?
A decode-time integer expression fold would take a basic block whose interior is a chain of full-width 32-bit integer ops, build one dataflow expression… - Interpreter Dispatch: What Has Actually Been Measured
Companion docs: wasm-stack-threaded-code.md (the proposal this constrains), aoe-performance-optimization.md (the older experiment table),… - Loop Idiom Superinstructions
Companion docs: interpreter-dispatch-perf.md (why "fewer dispatches" is the only lever, and why it is not automatically a win),… - Loop Microbenchmark Harness
Companion docs: interpreter-dispatch-perf.md (the four timing passes that resolved nothing, and why), page-compile-design.md §14 (CASE_CHAIN's whole-app… - Memory Map — wine-assembly vs Windows 98
The shared memory is fixed at 8192 WebAssembly pages. - Writing
$nextout at its 405 call sites — a measured negative
Measured 2026-09-01 on main (working tree at ad5d5391 plus other lanes' in-flight edits; both A/B artifacts built from the same tree, so nothing but this… - Non-GDI Work Plan
Status: planning baseline for work that can proceed while the software-GDI rewrite is owned separately. - Page compilation: address-ordered threaded code with a parallel index
Status: experiment, branch perf/page-compile, forked at 7d471df9. Baseline worktree pinned at the same commit: /private/tmp/wa-pagecomp-base. - Paint refactor — remaining work
The longer-term pixel-storage and rasterization architecture is tracked in docs/software-gdi-design.md. - Performance: the consolidated ledger
Every performance effort on this interpreter, what it measured, and its verdict. Read this first; the linked docs carry the detail and the raw numbers. - Recovery execution — 2026-09-10
User authorized active-owner checks, review, benchmarks, and integration of useful recovered work. No deployment or cleanup of old worktrees is included. - Refactor: Controls as Real Windows, JS as Dumb Renderer
Status: STEPs 1-6 done. STEP 8 partially done (find dialog only — first dialog with zero JS-side state mirroring). - Carrying toyvm's live region JIT into the WAT x86 interpreter — a measurement study
Recommendation at the end is DO A NARROWER THING.** - Replicated dispatch (
repl_tailcall) for the main emulator
messageboard.txt before touching src/*.wat or lib/compile-wat.js. - RichEdit Compatibility Task Design
Status: active task design for making native RichEdit usable across WordPad, installers, and other Win9x-era apps. - Runtime TrueType hinting design
TrueType programs run in WAT at runtime. They are not converted into bitmap strikes during the build, and this work does not change the independent MS… - Safari Private Browsing performance
Safari Private Browsing can make Wine-Assembly startup dramatically slower when its advanced tracking and fingerprinting protections are active. - Scalable font design
Design started 2026-08-14. Assets are committed, fonts/substitutions.json is written, and src/10c-truetype.wat parses font tables and derives metrics:… - Semantic expression fusion benchmark brief
Can a stack or register interpreter profitably lower short arithmetic sequences to expression dataflow, instead of dispatching one handler per guest opcode? - Silent-handler inventory history
The build gate in tools/check-silent-stubs.js rejects additions or mutations to straight-line handlers that cannot delegate work, publish output, branch,… - Site and SEO work: what is built, how it is generated, what is left
The public site of Wine-Assembly: how the app, article, design and story pages are generated, what was done to make them findable, and the open list. - Software GDI rasterizer design
Implementation started 2026-08-12. WAT now owns canonical bitmap and window surfaces, GDI object/DC state, regions and explicit clipping, native-format… - The three backends over the twenty-program set (2026-09-01)
One measurement of every optimization the toy VM has, on the same twenty programs (tools/toyvm/bench-set-20.txt), on the same evening, at commit f2c76827. - Dead flag writes in the toy VM
Most flag writes are never read. A basic block is typically a few arithmetic ops and then a compare and a branch, and only the compare's flags are ever… - Moving the toy VM's decoder into wasm
tools/toyvm/decode.js opens by saying the decoder is on the host deliberately: - The toy VM: comparing dispatch shells on real 16-bit programs
Companion docs: interpreter-dispatch-perf.md (what the production interpreter measured, and why "fewer dispatches" is not "faster"),… - Lazy flags in the toy VM
Every arithmetic instruction called an eager helper that computed all six arithmetic flags and stored the word. - Pinning the register a handler reaches for
A guest register lives in a wasm global. A handler gets at one by calling $rget16 / $rset8 / … with an index, and those helpers are a br_table over eight… - The region JIT, in the live run loop
through DosSession's afterSlice hook, builds a serializable bundle ({samples, mem, regs, machine, ...}), hands it to a backend, and installs what comes back. - Not running the loop that waits
After fusion, dead-flag elimination and trace blocks, the obvious next move was to find the biggest remaining handler pair and fuse it too. - The loops that are worth folding are not the ones that stop
The spin fold does not run a loop that cannot end. - Superinstructions in the toy VM's threaded code
Threaded code pays for every operation twice: once to do the work, and once to get to it. - Compiling through a conditional branch
A threaded-code op pays a dispatch. A block transfer pays a dispatch and then something else on top of it: the successor's arena address is loaded out of… - Pricing a trace JIT before building one
The dispatch shootout asks how to make the interpreter's dispatch cheaper. This asks a different question: what is left once dispatch is gone entirely? - The expression-tree fold
--tree-fold, off by default. tools/toyvm/tree-fold.js, the pass in compileProgram, test/test-toyvm-tree-fold.js. - 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. - Tracing & debug flag performance
Every --trace-, --break, --watch, and --count flag adds per-block or per-API work to the emulator hot path. - Indexed-color undithering design
Status: design only. No implementation is implied by this document. - Virtual LAN party design
This document specifies a virtual LAN for unmodified Win32 games running in Wine-Assembly. Liquid War 5.6.2 is the first target. - WebAssembly Engine Feature Support
The build already ships two wasm binaries — build/wine-assembly.wasm (return_call) and build/wine-assembly.compat.wasm (call; return) — chosen at load… - Wasm Stack-Threaded Code Proposal
Measured constraints this proposal must respect are in interpreter-dispatch-perf.md. - Struct layouts in WATX — deleting the hand-spelled field offsets
12,587 memory sites reach their fields through hand-spelled arithmetic. tools/struct-offset-census.js counts and classifies them. - WATX migration — Milestone 2 syntax/opcode gap census
Corpus-driven gap list for docs/watx-migration-plan.md §2.3. - Milestone 4 — compiler-Worker measurements
What this answers: the open gate in docs/watx-migration-plan.md §"Milestone 4 — Browser source compilation" — "Measure cold compile time and peak process… - WATX migration plan
Status: Phase 1 complete — WATX is the canonical compiler (cutover 2026-08-31 at byte identity). - Memory regions in WATX — deleting the magic numbers
Milestone 6 of docs/watx-migration-plan.md. This document decides the feature before the bulk of it is written. - WATX typed pointers, casts, and layout unions
Status: specification, user-signed-off 2026-09-01. Three tiers, each with its own oracle. - Widescreen / high-res opportunities per app
Status 2026-08-31. This is a backlog document: no app settings, config seeds, or byte pokes have been applied. - Win16 Entertainment Pack source provenance
This document records the source and byte-level verification for the three Entertainment Pack files repaired locally on 2026-08-20. - Win16 native Windows 98 visual audit
This audit compares every Win16 application registered in lib/apps.js with the same binary running natively in the pinned Windows 98 v86 reference. - WAT-native WinHelp design
Status: implementation in progress. Phase 1's WAT-owned file buffer, bounded-reader foundation, directory B+tree parser, and raw VFS load path are… - x87 micro-region stack representation shootout
This experiment asks a narrow question: once the decoder has proved a balanced x87 region, how should that region carry ST(i) values around its loop? It… - Realistic x87 region representation benchmark
This is the finite-trip follow-up to docs/x87-microregion-representation-bench.md.
Reverse-engineering notes, one per application
- Abe's Oddysee demo
The registered abedemo payload launches test/binaries/shareware/abe/installed/abedemo.exe and explicitly mounts its four DDV movies plus c1.lvl, r1.lvl,… - Sid Meier's Alpha Centauri Classic (Windows 95 ISO)
Established against SidMeierAlphaCentauriClassic-Windows95.iso, Joliet volume SMAC-E1_0Z (2,781 files). - Age of Empires II Trial
binaries/shareware/aoe2/aoe2_ex/EMPIRES2.EXE now reaches live Random Map gameplay through the registered aoe2 app on the same asset manifest used by the… - Atomic Bomberman alpha demo
The tested April 7, 1997 alpha is the ready-to-run BMANDEMO tree from the local Windows 98 A-D compatibility archive documented in sources.md. - Baldur's Gate previews
Three distinct official previews are useful local-only Infinity Engine fixtures. - Beneath a Steel Sky — GOG Windows ScummVM
The local fixture is the GOG offline installer recorded under gog-free-beneath-a-steel-sky in test/candidate-corpus/manifest.json and sources.md. - Black & White 2 demo
test/binaries/win98-games-a-d/Black and White 2-DX9-D3D.exe is the 647,216,200-byte Black & White 2 demo InstallShield 11 self-extractor. - Bricks I (
bricks.exe)
App id bricks. test/binaries/wep32-community/Bricks/bricks.exe, a Klotski remake. - Broken Sword demo
tools/install-broken-sword-demo.js runs the original SETUP.EXE, accepts its DirectX check, follows the launched GAMECFIG.EXE into the full-screen… - Caesar III demo
The playable payload is produced by the original Sierra/Impressions demo installer chain documented in sources.md: - Captain Claw Demo
The local fixture starts with the unchanged 11,275,313-byte claw_demo.exe self-extractor linked from sources.md. - The Curse of Monkey Island demo
The local original demo comes from the Windows 98 A-D distribution linked in sources.md. The package and playable files remain local-only. - Classic WordZap local idle audit
2026-09-10, source304, headful Chrome,1280x900. Game/Start command40003 alone is not proof of a running round: it shows the startup art/instruction overlay. - The Elder Scrolls II: Daggerfall (GOG build 28043)
The local fixture came from the Internet Archive gog_collection item and is pinned in test/candidate-corpus/manifest.json as SHA-1… - Darkstone demo 1.0
The local package comes from the Windows 98 A-D archive recorded in sources.md. - Deus Ex demo — local candidate
The official 1.002f demo installer is pinned in test/candidate-corpus/manifest.json from Archive item DeusExDemo. - Diablo retail (CD install, v1.00)
Companion to diablo-shareware.md — same engine, but the retail disc adds the install flow and the CD check. - Diablo Shareware
test/binaries/candidates/diablo-shareware/installed/, registry id diablo_shareware (lib/apps.js). - Diablo II Shareware demo
The local package is Blizzard's DiabloIIDemo.exe: - Dungeon Keeper demo
The local Windows demo comes from the Windows 98 A-D archive recorded in sources.md. - DX-Ball (
dxball.exe)
App id dxball. test/binaries/candidates/dxball/installed/dxball.exe, imageBase=0x400000, loads at 0x400000 (delta 0, so original VAs are runtime VAs). - Elasto Mania 1.0
Registered executable: test/binaries/candidates/elasto-mania/Elma/Elma.exe, app id elasto_mania, image base 0x400000. Imports DSOUND.dll!DirectSoundCreate. - The Elder Scrolls: Arena (GOG)
The local fixture comes from the Internet Archive gog_collection item pinned in test/candidate-corpus/manifest.json. - EmPipe local idle audit
2026-09-10, source304, headful Chrome,1280x900. Game/New command100 resets the title screen; it does not start the pipe board. - Fallout Win95 Interactive Demo
The fallout-demo candidate pins falldemo.zip from the Archive.org FalloutDemo item (SHA-1 214c6b8931f75aa2a9a11f521a26b0cb8e565ad9). - Flight of the Amazon Queen — GOG Windows ScummVM
The local fixture is the GOG offline installer recorded under gog-free-flight-of-the-amazon-queen in test/candidate-corpus/manifest.json and sources.md. - Four Stones
Registry fourstones, test/binaries/wep32-community/Funpack/FourStones.exe with FunPack.dll. Win32 Connect Four game, window400x300 at desktop(20,20). - Funtris local idle audit
2026-09-10, source304, headful Chrome. This is a realtime falling-tetromino game. - Grand Theft Auto 2 Wild Demo
f8fc0a9653932f008a03e56ea892fb31dbeb98d228cc4df61b910bcb35d08d21. - Half-Life Uplink demo
The local-only installer is test/binaries/candidates/half-life-uplink-installer/hluplink.exe (50,872,079 bytes). It is a Win9x InstallShield package. - Heroes of Might and Magic II (demo)
test/binaries/candidates/heroes-2-demo/files/H2DEMOW.EXE, registry id heroes2_demo (lib/apps.js). Reaches the adventure map headlessly — see - Heroes of Might and Magic III Demo
The local-only candidate is the March 1999 demo. - Icewind Dale demo — local candidate
The official English demo is pinned in test/candidate-corpus/manifest.json from Archive item icewind_dale_eng_demo. - Icy Tower v1.3.1
Local app ids icy_tower and icy_tower_installer. - Jardinains! v1.2
Jardinains! 1.2 is a Win32 Blitz/DirectDraw Breakout-style shareware game. - Jazz Jackrabbit 2 Shareware 1.23s
The source fixture is the unchanged 20,069,847-byte J2swc123.exe; installed files remain local test material and are not a replacement distribution. - Liquid War 5.6.2
Binary: test/binaries/candidates/liquid-war/LW5/lwwin.exe, image base 0x00400000. - Little Fighter 2 v1.9
Local-only app ids little_fighter_2 and little_fighter_2_installer. - Lure of the Temptress — GOG Windows ScummVM
The local fixture is the GOG offline installer recorded under gog-free-lure-of-the-temptress in test/candidate-corpus/manifest.json and sources.md. - MechWarrior 3 demo
for std::_Lockit (??0_Lockit@std@@QAE@XZ and ??1_Lockit@std@@QAE@XZ). - Motocross Madness demo
The browser build can reach a live race without Wine. With a persisted profile, the reliable 640x480 guest-coordinate route is: - NetHack 3.4.3 for Windows
This target is the native Win32 graphical build, NetHackW.exe, from the official NetHack 3.4.3 package. - Pocket Tanks v1.6 shareware
Local-only app ids pocket_tanks and pocket_tanks_installer. - Quake II 3.14 demo
C:\WINDOWS\Desktop\Quake2 Demo; the playable executable is Install\Data\quake2.exe beneath that directory. - Rodent's Revenge 2000 (VB6 remake)
test/binaries/wep32-community/Rodent2000/Rodent2000.exe — James Emmrich and Alexander Popov, 2002. - RollerCoaster Tycoon (shareware/demo)
test/binaries/shareware/rct/English/RCT.exe — Chris Sawyer / Hasbro Interactive, - Shadow Warrior Classic Complete (GOG build 2.0.0.7)
The local fixture came from the Internet Archive gog_collection item and is pinned in test/candidate-corpus/manifest.json as SHA-1… - Snood 2.2W
Local app ids snood and snood_installer. The original package is test/binaries/candidates/snood/SnoodWin22Install.exe; the optional installed browser… - Solitaire (NT debug build in Entertainment Pack)
2026-09-10 mobile assertion diagnosis; no runtime fix implemented. - StarCraft shareware
The official electronic demo preserves the CD build's accumulator-driven catch-up-loop structure, with relocated code and globals: - Total Annihilation Demo
The local fixture is the unchanged 21,540,864-byte Total Annihilation.exe self-extractor linked from sources.md. Wine-Assembly runs that executable directly. - Total Annihilation demo
The original Total Annihilation.exe self-extractor is retained locally at test/binaries/candidates/total-annihilation-demo/. - Ultima IV: Quest of the Avatar (GOG)
The local fixture comes from the Internet Archive gog_collection item pinned in test/candidate-corpus/manifest.json. - Rodent's Revenge (WEP2, 16-bit)
test/binaries/wep16/WEP2/RODENT.EXE — Microsoft Entertainment Pack 2, 1991, Christopher Lee Fraley, written in Visual Basic 1 (NE, links VBRUN100 plus the… - Worms 2 October demo
Local app id worms2_demo. The original setup media is prepared beneath test/binaries/candidates/worms-2-demo/installer-10oct/; any installed browser…