Sid Meier's Alpha Centauri Classic (Windows 95 ISO)

Established against SidMeierAlphaCentauriClassic-Windows95.iso, Joliet volume SMAC-E1_0Z (2,781 files).

The autorun error is on the disc

The root AUTORUN.INF launches AUTOMENU.EXE. That wrapper reports:

There was a problem loading one or more graphic images. Did you forget to copy the DATA directory?

The mounted ISO has no directory named DATA. Its AUTOMENU.APM also embeds registry paths for an unrelated Jane's Combat Simulations title, so this is a bad third-party menu packaged into the image, not a failed ISO mount. The disc's copy of D:\PROGRAMS\TERRAN.EXE is the original v1 executable, while D:\Patch\smacp4e.exe is Firaxis's official v4 WinZip self-extractor. The production path must run that original updater inside Wine-Assembly; mounting its ZIP payload directly was useful for compatibility diagnosis but is not an acceptable replacement for installer emulation.

Startup API sequence

TERRAN.EXE ships D:\PROGRAMS\ARIALN.TTF and creates the relative resource name ARIALN.FOT while its current directory is D:\PROGRAMS. The CD-ROM is immutable, so CreateScalableFontResourceA validates the TTF and records a process-local FOT-to-TTF association even when it cannot persist the FOT next to the source. Subsequent AddFontResourceA("arialn.fot") and RemoveFontResourceA resolve through that association.

The next startup stage opens multimedia content and calls mmioSetBuffer(hmmio, NULL, 16384, 0). Wine-Assembly's host-backed VFS reads are synchronous, but the API must still accept and reserve the requested internal storage. With both calls implemented, the exact ISO runs for the full 180-second probe without an unimplemented-API crash (104,697 API calls, 2,721 batches) and creates the main 640x480 DirectDraw surface.

The 30 MB D:\movies\opening.wve exposed two independent emulator bugs. ISO files are fetched lazily in the browser, but mmioRead originally treated the provider's pending result as EOF instead of parking and retrying the Win32 call. The proof was the first four-byte read returning zero followed by the player's mmioSeek(-4, SEEK_CUR) still starting from position zero. mmioRead now uses the same pending-I/O retry contract as ReadFile; after resumption the player reads the WVE header. The decoder later switches to buffered mmioAdvance, which crosses the same lazy-provider boundary, so its refill now parks and retries as well instead of presenting an empty buffer as end-of-file. Without that second retry the corrected movie animated until the next unloaded ISO extent and then remained forever on its white terminal frame; Escape only hid the truncated playback and exposed the already-working main menu.

The source movie is deliberately letterboxed at 400x192 inside Alpha's 640x480 exclusive primary. The terran.exe presentation profile crops that exact centred rectangle and aspect-fits it to the browser output. The crop is guarded by the 640x480 backing size, so Alpha's later resized menu and gameplay surfaces return to ordinary full-surface presentation without a game-state heuristic or pixel scan.

At true EOF the player has one final synchronization condition: its non-looping DirectSound voice must stop. A browser AudioContext created before user input can remain suspended, so its AudioBufferSourceNode never delivers onended. The host previously treated the mere presence of that stale source object as "playing" forever. Non-looping snapshot voices now retire from the same guest clock used by their play cursor even when onended is unavailable. A complete raw-ISO browser run reached the full-size main menu at guest time 342.9 seconds without Escape or any other injected input.

Once reads worked, the movie animated with severely corrupted horizontal bands. This was already present in both raw 640x480 RGB565 DirectDraw surfaces, so it was not a browser palette or presentation error. The EA TQI bit reader uses SHLD eax, edx, cl at one cached instruction address with a changing bit offset. The decoder had incorrectly sampled ECX while compiling the block and embedded that first count in the threaded instruction. Every later execution therefore reused a stale shift width and produced bad transform coefficients. The CL forms of SHLD/SHRD now carry an out-of-byte-range marker in decoded code and read CL & 31 in the execution handler. The original ISO now shows a coherent, animated Firaxis opening in Chrome; DisableOpeningMovie=1 remains a useful diagnostic shortcut, but is no longer required for startup.

Focused coverage lives in test/test-wat-font-resource.js, test/test-wat-mmio.js, and the cached-block SHLD/SHRD cases in test/test-x86-ops.js.

Menu and Quick Start gameplay

The v4 game menu draws its labels with WinG DIBINDEX COLORREFs (0x10ff0000 | paletteIndex). Keeping that qualifier through SetTextColor/SetBkColor and resolving it against the selected 8-bit DIB palette restores all seven formerly missing menu labels.

Quick Start also exposed three independent PeekMessageA contract violations. Host input excluded by one of the game's disjoint filters was being consumed instead of left for the later general poll, while synthetic WM_PAINT and timer messages were returned even when the requested range contained only WM_USER+1. Finally, timer and posted results left MSG.time untouched. SMAC peeks three disjoint ranges, sorts the three records by that timestamp, then removes the selected message; the missing timestamp made a due timer lose to an empty record and remain due forever. Filtered input is now retained, synthetic messages obey the same range, and every returned timer/posted message has a complete MSG tail. Ordinary empty peeks return within the current slice; the existing repeated-call spin detector still parks true idle loops.

After Planetfall, Alpha creates more than 300 simultaneous GDI objects. The old emulator-only 256-object table returned NULL for a valid 105x20x8 CreateDIBSection despite 14,634 free backing pages, producing “Unable to allocate draw-buffer.” GDI object and DC-state capacity is now 512. The exact ISO with the official v4 executable was driven in Chrome through Quick Start, Planetfall, base naming, and into the interactive Mission Year 2101 map with the tutorial and its map unit visible and no runtime crash; the final native census was 314/512 objects and 225/512 DC states.

Gameplay frame production

A browser-controlled complete turn advanced Mission Year 2101 to 2102. The page compositor remained around 26 fps, but the 800x600 game image changed in bursts at roughly 6--7 fps, with multi-second gaps while the turn simulation ran. Idle map pixels do not change at all, as expected for this turn-based game, so page/rAF fps must not be reported as Alpha's gameplay fps.

This is not a low frame cap in TERRAN.EXE. The active animation loop at 0x0045bc3b..0x0045bdbd draws one frame, services messages through 0x005e5700, and repeats until timeGetTime() - frame_start reaches the global interval at 0x00671a58. That interval is initialized to 20 ms (50 fps), with one state using half the interval. Exact counters over one measured turn saw 975 passes through the clock/pump wait but only five completions of this particular short animation; the whole turn made 1,179 timeGetTime calls. No Sleep, GetTickCount, DirectDraw Flip, Lock, or Unlock call occurred in the measured window.

Guest execution is the limiting work. A handler-histogram repeat retired about 142--157 million guest basic blocks in 17.5--17.8 seconds. The leading blocks are game-state evaluators, not timing or host presentation:

Therefore changing canvas upload or browser presentation cannot recover the missing game frames: Alpha redraws only after large synchronous rule and map evaluation passes, and those passes run at interpreter throughput. The next useful optimization target is the repeated vehicle-evaluation call graph (or general dispatch/register throughput), not the 20 ms clock wait. The 25-record search is measurable but too small on its own to explain the gap. These absolute timings came from a loaded development machine and profiling adds overhead; the attribution and call-count ordering were stable across repeats.

Chrome CPU sampling and clock A/B

A 200-us Chrome CPU sample over the same complete turn put 13.2% of self time in threaded-interpreter $next, 11.1% in PeekMessageA, and 10.3% in the host performance.now() wrapper reached through get_ticks. Generic branch, register, effective-address, and guest-memory handlers account for much of the remaining profile. By contrast, $decode_block was 1.1%, $g2w 1.1%, and page resolution 1.6%; neither decode churn nor address translation is the primary limit. The sampling harness's getImageData consumed 2.7% and is not emulator work.

The timer path contained a real duplicate clock sample: $timer_check_due refreshes $tick_count, then its no-WM_TIMER fallback called $mm_timer_due_slot, which refreshed it again. An isolated build reused the first sample for that fallback while retaining a fresh sample for standalone multimedia-timer polling. Host clock self time fell from 10.3% to 5.9%, but the measured turn changed only from 17.55 s to 17.39 s (about 0.9%, within run-to-run noise). The game simply performs more polls while waiting for the same wall-clock deadline. This cleanup may still be worthwhile, but it is not the frame-rate lever.

An isolated spin-park prototype then let repeated clock reads retain their evidence across PeekMessageA, while every other Win32 call still reset the run. At the production confidence threshold (K=8), 1 ms and 5 ms parks fired only 75 and 62 times and changed nothing measurable. At K=2, a 1 ms request completed the turn in 17.53 s with 24.7% Chrome-profiler idle time; 5 ms took 17.64 s with 26.2% idle. The unparked profile had about 1% idle. Content kept changing and Mission Year advanced in both arms. Thus yielding after two identical same-site/same-millisecond reads can reclaim roughly one quarter of browser CPU without lengthening this turn, but 5 ms is no better than the safer 1 ms request because browser timer scheduling already coarsens the yield. That global K2 form remained a prototype: pure guest computation between two clock calls is invisible to the API-only detector, so K=2 needs broader game-corpus false- park testing before becoming a global default.

The remaining frame-rate work is reducing dispatches with narrowly measured instruction fusion or moving to a native/dynamic translation tier. Making $next cheaper, source-inlining it, and splitting hot memory-accessor fast paths have already measured neutral or slower on other games; they are not experiments to repeat. Optimizing canvas presentation, growing the decode cache, or special-casing only the 25-record search likewise has little support in this profile.

A subsequent exact handler census found three frequent instances of ADD {EDX,EBP,ESI}, [EAX*2+disp32]. Dedicated handlers 444--446 fuse the SIB address calculation, dword load, addition, flags, and register write for only those three forms. A deterministic handler microbenchmark improved by about 64%, but a complete Alpha turn improved by only about 0.57%, within the run's noise. The narrow fusion is a safe dispatch/memory-access reduction and a useful template for further measured pairs, but this individual site is not a material gameplay-FPS lever.

An adaptive clock-spin park was accepted as a CPU-courtesy measure, not as an FPS fix. A call site must first produce eight identical clock reads with the same return address/stack and no meaningful API work; after that proof, the same site re-arms on two reads in later milliseconds. Empty PeekMessage passes are neutral, but a delivered message breaks the evidence. The browser requests a 1 ms park (adjustable for diagnostics); timer coarsening may make the real pause longer. In a direct-disc diagnostic run using the official v4 payload overlay, the adaptive K8-to-K2 path completed a turn in 17.48 seconds, recorded 112 parks and 65 changed screen samples, and advanced to Mission Year 2102 without a crash. This is essentially the same wall time as the unparked ~17.55-second result: the change avoids some proven useless polling but does not make Alpha produce animation frames faster. Heroes II, GTA2, and SkiFree gameplay plus the focused message/timer regressions retained progress.