Myth: The Fallen Lords (Bungie, 1997) — demo and retail ISO
Status 2026-10-05 (NEW-GAME-MYTH-20261005): both the official Win32 demo and
the retail CD reach in-level 3D gameplay headless with ordinary mouse and
keyboard input. Evidence: scratch/runs/20261005-myth-tfl-retail-iso-gameplay/
(retail, reviewed, result.json) and scratch/runs/20261005-myth-tfl-demo-gameplay/
(demo; not a registered candidate, so no result.json), and the browser route
scratch/runs/20261005-myth-tfl-retail-browser/ (reviewed). FPS not measured.
Registered app myth_tfl (localhost-only)
Build or rebuild the local tree with node tools/prepare-myth-tfl.js (runs the
disc's own Setup.exe headless, ~5 min; --manifest-only rewrites just the cue
and .wine-assembly-browser.json from an existing installed/). A fresh run
reproduces the installed tree byte for byte.
lib/apps.js LOCAL_CANDIDATE_APPS; refused by tools/deploy-berrry.js. Local
media under test/binaries/candidates/myth-the-fallen-lords/ (gitignored):
installed/— the Small install Setup.exe writes (copied out with--save-vfs-prefix='c:\program files'), mounted atc:\by.wine-assembly-browser.json(c:\tags\tags.gor,c:\modules\tcpip.dll, …).sources/myth-tfl.cue— oneMODE1/2048track overmyth-tfl.iso; thecdAudioconfig mounts it lazily asD:MYTH_TFL(HTTP Range in the page), so the 538 MB ISO is never loaded whole.disc/(the fetch tool's extraction) is not used by the route.
Browser (headless Chrome, web-input-probe.js --app=myth_tfl, served by
tools/dev-server.js for Range support): menu at ~48 s, level ~110 s real time.
In real time the enemy is already crossing the bridge by the time the level is
on screen. Input verified there: A turns the camera, F8 key list, minimap click
moves the view; Enter (Select All) reached the guest as WM_KEYDOWN 0x0D with no
visible selection. Unit select/move was verified on the CLI.
Sources
| What | Where | Checksum |
|---|---|---|
| Demo installer | https://archive.org/download/MYTHDEMO/MYTHDEMO.EXE (32,647,564 B) | md5 a38fe6ef950e22c1c7e7d231c29d0afc, sha256 d6b85468bed2714685e827311047083339f4123f03baa804ebe039a5e52b2771 |
| Retail ISO | candidate myth-the-fallen-lords (tools/fetch-candidate-corpus.js), 538,191,872 B |
md5 1edda1890e32e7bde09ce3bf563c0479 |
tucows_205995_Myth_-The_Fallen_Lords-Demo on archive.org is the Mac .sit; skip it.
Myth II demo (MythIiSoulblighterDemo) was not needed.
Both installers are MindVision Installer VISE (vise32ex.dll); 7-Zip cannot
open them, so they must run in the emulator. Demo: 52 MB install, payload is
one tags/tags.gor (51,594,033 B). Retail "Small" install is 31 MB
(myth_tfl.exe, tags/tags.gor, tags/scrap.gor, DLLs); artsound.gor
(320 MB) and cutscene.gor (114 MB) are read from the CD (D:\TAGS\, label MYTH_TFL).
Modules and API profile
myth_tfl.exe(demo 713,216 B; retail 714,752 B), image base 0x400000. DirectDraw 8bpp software renderer, or its own 3dfx renderer through our built-in glide2x (the default since 2026-10-06; see "3dfx (Glide 2) renderer" below; the disc's real glide 2.4.3 driver is never loaded), DirectSound, DirectInput keyboard device (acquired, state never read in-game — keys arrive as WM_KEYDOWN), WSOCK32.uber.dll— Bungie protocol switch (GetIndexedProtocol,Protocol*), LoadLibrary'smodules\tcpip.dll.mclient.dll— bungie.net client, imports uber. Both must be real PEs (lib/dll-registry.jsAPP_LOCAL_DLLS); as stubs the main menu traps onGetIndexedProtocol.- In-game input: mouse buttons via window messages, sampled once per rendered
frame. Headless at 200 ms/batch an in-game frame is ~250 batches (~50 guest s),
so a
--inputpress must be held across a frame (mousedown… 400 batches …mouseup); same-batchclickand short presses are silently dropped. Menus run fast enough thatclickworks there.
Emulator fixes this title needed
_hwrite(api id 4093) — VISE installer writes with it; delegates to_lwrite.CreateFileon an existing directory must failERROR_ACCESS_DENIED(5). Myth's dir-exists probe (exe+0x412b40: CreateFile(dir, GENERIC_READ, OPEN_EXISTING);GetLastError()==5||==32⇒ exists) got 2, then CreateDirectory → 183 and the tag-file layer reported "error #2 while writing (#0,#1228)", ending ininitialize_myth_for_new_map(NULL) failed.(string 0x4a0014) → exit(-1) with no visible message. Retail recursed onC:\TAGS\PRIVATEinstead.uber.dll/mclient.dllapp-local DLLs (above).- Harness:
--iso-exenow reports the disc path (D:\Setup.exe) from GetModuleFileName — VISE copies "from its own directory" and failed with "cannot open the source file 'C:\mclient.dll'".--save-vfs-prefix=added: an unfiltered--save-vfsafter an ISO install exports every disc file Setup read and filled the host disk.
Named addresses (demo exe)
0x411e90set-error(type, code, fmt, …);0x411fc0error-pending check (global[0x4b64c0]), returns 1 ⇒ init fails.0x412b40directory-exists probe;0x412750CreateDirectory wrapper.0x425e60new-game/map setup;0x43b570initialize_myth_for_new_map.0x40d5b0halt-with-message (prints nowhere visible) →0x4256f0→ exit(-1).
Reproduction
Retail (after a Small install into retail-installed/, ISO on D:):
node test/run.js --exe=<retail-installed>/myth_tfl.exe \
--vfs-include='*.dll,tags/**,modules/**,*.ini' --iso=<myth-tfl.iso> \
--quiet-api --stuck-after=0 --max-seconds=280 --max-batches=84000 --no-close \
--input=12000:mousedown:320:240,12100:mouseup:320:240,16000:mousedown:320:240,16100:mouseup:320:240,20000:click:404:272,21900:mousemove:310:258,22000:mousedown:310:258,22100:mouseup:310:258,44000:mousedown:320:400,44100:mouseup:320:400,75000:png:/tmp/game.png
Intro ≈ batch 11900, menu ≈ 21800 (after dismissing "no network modules"), briefing ≈ 40000, Crow's Bridge in-level ≈ 75000. F8 = key list; W/S/Z/X move, A/D turn, Q/E orbit, C/V zoom.
Network modules notice (fixed 2026-10-05)
"Networking is unavailable because no network modules were found in the
modules folder" came from UBER.DLL's DllMain (0x10002a7a orig): it chdirs to
.\Modules, enumerates *.*, LoadLibraryAs each DLL and asks for
NMGetModuleInfo, then restores the CWD. Three emulator faults stacked:
- CLI:
run.jsmounted the app's files after the static DllMains ran, soC:\Modulesdid not exist yet (the page loads files first). It now mounts the manifest beforeloadDlls. - VFS: a directory that only exists because a mounted file lives under it was
refused by SetCurrentDirectory/GetFileAttributes (
_isDirectory). - Both hosts:
callDllMainruns an initializer synchronously and treated the LoadLibraryA yield (reason 5) as "resume", so LoadLibraryA returned a stale EAX (0x074ff7b0) and the load was serviced only after DllMain had given up.serviceLoadLibraryYieldSync(lib/process-boot.js) now maps the DLL in place when its bytes are resident; anything needing a fetch keeps the async path. (--trace-apistill prints the stale EAX for that call: it records the return at yield time.)
After the fix "Multiplayer Game" is enabled in the main menu.
Performance and audio (browser, headless Chrome, 2026-10-05)
- Metric:
WinePerf.snapshot().guestFps= PRESENT/s (explicit DirectDraw presents). In-level Crow's Bridge: 29.3–30.2 PRESENT/s across five samples over ~25 s; page 60 fps, ~26–34M blocks/s, throttled 0%. Myth runs its game loop at 30 ticks/s, so this is at its cap. Menu presents only on change (0/s idle). Headless Chrome only: no display/Xvfb on the box, so no headful number; treat as indicative, not as a felt frame rate. - Audio (fixed 2026-10-06): intro and menu music always played; in-level
was silent. Myth mixes into a few looping DirectSound rings through
Lock/Unlock. Once its heap had spilled into the sparse VirtualAlloc backing,
IDirectSoundBuffer_Lockrebuilt the guest pointer with the direct-window inverse (wa - GUEST_BASE + image_base), which names unmapped memory there:--fault-nullcounted ~2.8M writes from the mixer (EIPs 0x476e75/0x476fbf/ 0x476e68) to 0xa1b519c-0xa27ccb0, all landing in the NULL sentinel, so every voice refresh played zeros (browser probe: 67 plays/20 s, PCM peak 0, output 0/400). Fixed with$w2g; same window now: PCM peak 0.282, output non-silent 400/400 samples. Regression: test/test-directsound-lock-sparse.js.
3dfx (Glide 2) renderer (2026-10-06)
Myth has its own 3dfx renderer (C:\myth\render\render_3dfx.c) and picks it by
itself. 0x419a80 loads glide2x.dll (our built-in module) through 0x419db0,
which resolves 33 entry points by name and returns failure on the first NULL
(test eax,eax / jnz after each store, e.g. [0x4b63f0] =
_grDrawPolygonVertexList@8); it then calls grSstQueryBoards and sets
[0x4ab950]=1 ("3dfx available") when a board is reported. 0x46d1f0(type) is
"renderer usable": 0 = software always, 1 = 3dfx if available and the memory
tier [0x4ac13c] >= 1 (0x42a710: GlobalMemoryStatus.dwAvailPageFile - 8 MB,
capped at 32 MB, against tiers at 0x4a0910: 7 MB, then 12 + 1.5 MB). The
graphics prefs record (validated by 0x46d2e0) holds the renderer at +0x1c;
the defaults routine 0x46d380 takes the highest usable renderer, so a fresh
preference file uses 3dfx whenever Glide is complete.
What it needed from us:
grDrawPolygonVertexList(convex fan from vlist[0], 60-byteGrVertex; Myth fills its list withadd eax,0x3c). It is most of Myth's terrain: 2814 calls vs 280grDrawTriangleper 1000 in-level batches.grSstWinOpen(0, 7=640x480, 0, 0, 0, nColBuffers=1, nAuxBuffers=2)(esi=1is a constant at0x46027e). The release Glide 2.4 driver on the disc (glide 2.4.3/grtvgr.exe->Glide/Drivers/Voodoo/Win95/glide2x.dll,_grSstWinOpen@28at0x100094d0) only checks nCol+nAux against frame-buffer memory for 800x600/856x480/960x720 on <= 2 MB boards; ours reports 4 MB. We now accept 1-2 colour buffers (1 = single-buffered: front and back are one surface in both backends) and up to 2 aux buffers.- Sampling TMU RAM that was never downloaded: Myth draws from 0x0..0x13fff before (and without) any download there; the hardware samples whatever the RAM holds, so both backends now do too (zero-initialised) instead of failing.
Evidence: scratch/runs/20261006T003007Z-myth-3dfx-inlevel-final (CLI,
--glide-renderer=software); compare the 8bpp software frame in
scratch/runs/20261005-myth-tfl-retail-iso-gameplay/05-gameplay.png. Software
stays selectable in Myth's own Preferences.
Choosing the renderer (user-facing). A fresh install starts on 3Dfx.
To switch, open Preferences from the main menu, pick Graphics → Rendering
→ Software (or 3Dfx to switch back), and press OK. The next game uses it.
Give the menu a few seconds after OK before clicking: Myth reinitialises its
renderer there, and an immediate New Game click is ignored. The choice is stored
in Myth's own preference file C:\TAGS\LOCAL\PREF\GAME. Whether it survives a
page reload depends on the browser keeping the emulated C: drive; that was not
tested, because every probe starts a fresh profile.
Browser verification (2026-10-06, headless Chrome, SwiftShader WebGL):
- Default 3Dfx, Threads on:
scratch/runs/20261006T010423Z-myth-3dfx-web-threads-fixed. - Default 3Dfx, Threads off:
scratch/runs/20261006T011714Z-myth-3dfx-web-threads-off. - Preferences shows 3Dfx selected; Software selected + OK, then in-level
software rendering (Threads on):
scratch/runs/20261006T010821Z-myth-3dfx-web-switch-software. test/test-myth-byo-iso-web.jsPASS on the 3Dfx default (in-level is the filtered 3Dfx frame; camera turn and audio pass):scratch/runs/20261006T011945Z-myth-3dfx-byo-iso-web.
Threads crash (fixed b7acece2). With Threads on, Glide goes through
lib/glide-render-worker.js, which accepted only a 20-byte open packet. 8cd80d5a's
24-byte grSstWinOpen packet (word 5 = nColBuffers) made it throw
Invalid Glide open packet, trapping the guest at EIP 0x4602f6 (Myth's
grSstWinOpen). Safari reports the page-side frame instead
(lib/render-worker.js:54 _receive, which rejects with the worker's error).
Regression: test/test-glide-webgl-fallback.js.
Bring-your-own ISO in the page (2026-10-06)
Evidence: scratch/runs/20261006-myth-tfl-byo-iso-run6/ (reviewed). Regression:
test/test-myth-byo-iso-web.js (local-only, skips without the ISO; ~4 min).
Both were validated on e7c970ef, i.e. on Myth's software renderer: from
8cd80d5a the game picks its 3dfx renderer, and in the browser that is the WebGL
Glide backend, which crashed at the time of writing (claude:1863d2b5's open
fix) -- rerun this test once it lands.
- Drop
myth-tfl.isoon the desktop (window.wineMedia._fileInput): the insert dialog reads label MYTH_TFL, 538 MB, 312 files and offers 13 programs; the default is the autorun launcherStartup.exe, so pickD:\Setup.exe. - "Insert and keep" copies the ISO into OPFS; Setup's C: writes go to that
media's OPFS overlay journal (
[overlay] checkpoint: persisted N change(s)). The VISE pages advance with Enter; the final "Install DirectX 5.0?" box is Tab, Enter (= No); then Enter for Close. - VISE makes its Start Menu group only by Program Manager DDE
(
[CreateGroup("Myth The Fallen Lords")],[ShowGroup(...,1)],[ReplaceItem(...)],[AddItem(C:\Program Files\Myth_TFL\myth_tfl.exe, Myth The Fallen Lords, , 0, -1, -1, )], with wFmt = CF_TEXT). The virtual Progman used to ACK without acting (and refused CF_TEXT), so no shortcut and no icon; it now writes the .lnk (e7c970ef). The shell then shows a kept icon "Myth The Fallen Lords" (lnk:<media id>:c:\program files\myth_tfl\myth_tfl.exe). - That icon launches the installed exe with D: = the ISO (DRIVE_CDROM, MYTH_TFL): intro, menu, New Game, Crow's Bridge, A turns the camera, F8, in-level sound (54 plays/20 s, PCM peak 0.16).
Driver pitfalls (not product bugs): the new icon is appended below an 800 px
viewport -- scroll it into view; the desktop's open gesture is two click
events within 500 ms (puppeteer clickCount: 2 did not launch it); a Read Me
window may be open over the desktop.
Open
- CLI and browser both need a press held across one in-game frame.