MMX: what PODMMX.EXE accelerates
POD shipped in the first months of Intel's MMX. This page says what that meant in 1997 and what the MMX build changes in the code, with the matching routine of the plain Pentium build for each. State: traced for every MMX routine in PODMMX.EXE; measured for the coverage mask (identical to the Pentium routine) and the 16-bit channel mixer (matches a model built from the instruction semantics). Reusable code: GameFrame's crates/mmx (the instruction semantics).
POD and MMX in 1997
- Intel announced the Pentium processor with MMX technology on 8 January 1997: 166 and 200 MHz desktop parts, 57 new instructions, and PC makers launching systems "bundled with a variety of software designed for Intel MMX technology" (Intel press release, 1997-01-08).
- Intel's Pentium OverDrive with MMX came with a CD of sample titles that lists POD from UbiSoft Entertainment (Intel press release, 1997-03-03).
- POD is described as one of the first games to support MMX, shipped as an OEM version with Pentium and Pentium II MMX computers and some AMD K6 systems; the OEM 1.0 version had no 3dfx support and no network mode, which the retail 2.0 added (Wikipedia, "POD (video game)", citing Next Generation No. 23, November 1996). The disc here is that 2.0 (
POD2_0), with both. - Giant Bomb says the game was "designed with the Intel MMX processors in mind" and was included as a box set on many computers (Giant Bomb).
The builds on the disc
Every renderer has a Pentium build and an MMX build, linked the same day (1997-03-07, FINDINGS.md). Counts are of MMX instructions (movq, movd, p* on mm registers, emms) in a linear sweep (measured, ./gameframe run insns BINARY '^p[a-z]+ mm|^emms|^movq|^movd').
| Renderer | Pentium build | MMX instructions | MMX build | MMX instructions | What the MMX code covers |
|---|---|---|---|---|---|
| Software | WINPOD.EXE | 1 (a stray decode) | PODMMX.EXE | 813 | sound mixer, coverage mask, clears, textured spans |
| 3dfx Glide | WPOD3DFX.EXE | 2 | PODX3DFX.EXE | 525 | sound mixer only |
| ATI 3D Rage | WPODATI.EXE | 1 | PODATIX.EXE | 816 | sound mixer and the software renderer |
| S3 ViRGE | WPODS3D.EXE | 1 | PODS3DX.EXE | 813 | sound mixer and the software renderer |
Inferred from where the instructions sit: PODX3DFX.EXE has one block the size of PODMMX.EXE's mixer, because the 3dfx card draws everything; the ATI and S3 builds keep the software span routines (3d-cards.md says why) and so keep their MMX versions too.
All MMX code is in hand-written assembly modules (no compiler of 1997 emitted MMX; Watcom 10.6 built the C). Every routine ends with emms before returning to floating-point code.
The MMX routines of PODMMX.EXE
25 functions hold MMX instructions, in two modules (observed: instruction listing with --functions, then each routine read). Ghidra export: analysis/export/PODMMX.EXE.
Sound mixer (0x4d1460–0x4d2819)
The Pentium build mixes all channels in one routine, FUN_004df88a, with the x87 FPU: each sample is multiplied by a double volume at channel +0x7c, and the result is turned into an integer by adding a large constant and reading the low word of the stored double (observed). The MMX build replaces it with a C dispatcher (FUN_004bfea4 and FUN_004c0138, one per output path) that calls one assembly routine per channel kind:
| Routine | What it does (observed) |
|---|---|
FUN_004d1460 | 16-bit mono channel at pitch 1.0 into the stereo mix: 4 samples per loop; pmulhw by left and right volumes, punpcklwd to interleave, paddsw into the mix |
FUN_004d1795 | The same at any pitch (8.8 fixed-point step, one sample at a time gathered with punpcklwd/punpckldq); also writes to the effect send buffer when asked |
FUN_004d152c, FUN_004d18d5, FUN_004d1a63, FUN_004d1c0d, FUN_004d21a8, FUN_004d23cf, FUN_004d259e | Variants of the two above for the other channel kinds the dispatcher selects (+0x38, +0x3c flags, environment 6); same instruction pattern |
FUN_004d150b, FUN_004d1516, FUN_004d1521 | Empty (only emms): placeholders for channel kinds not mixed |
FUN_004d2144 | Fill the mix buffer with one 32-bit value, 8 bytes per store |
FUN_004d217b | Clear part of the effect delay line |
FUN_004d1e71 | Reverb for environments 1 to 4: a ring of 0x8000 bytes read 0x4000 bytes behind (0x2000 or 0x1000 in two of its five modes), added with paddsw and fed back at 3/4, 1/2 or 1/4 (psraw by 1 or 2) |
FUN_004d2004 | Environment 5: a delay that sweeps between 0x400 and 0x2000 bytes in steps of 8 every 200 blocks (psubsw, feedback psraw 2): a flanger |
FUN_004d20d0 | Environment 6: an echo from the send buffer, fed back at 1/2 |
FUN_004d26d1 | A 32-tap filter (pmaddwd, sum >> 13) whose result is added to one stereo side and subtracted from the other (paddsw, psubsw): a stereo widener |
The channel record is 0x84 bytes (observed, the dispatcher steps by 0x21 words): +0 position (8.8 fixed point, in samples), +4 step, +8 end, +0xc loop start, +0x10 sample data, +0x14 volume, +0x20 pan (0 to 128), +0x28 loop flag, +0x2c playing. The global DAT_006d66e8 selects the effect environment (1 to 6).
The volumes come from imul bl, a signed 8 × 8-bit multiply: left = vol × (128 − pan) × 4, right = vol × pan × 4, each truncated to a signed 16-bit word (observed). With pan at 0 or 128 the factor 128 reads as −128, so the loud side has its sign flipped (measured: the model below only matches with this rule). It is inaudible on its own.
Coverage mask and clears (0x4dde10–0x4de1e3)
The software renderer draws front to back and keeps one bit per screen pixel of what is already covered (0x50 bytes per row: 640 pixels). Spans that would land only on covered pixels are dropped (observed).
| MMX build | Pentium build | What it does |
|---|---|---|
FUN_004dde10 | FUN_004dcd70 | Mark a list of spans (one per row, 0x20 bytes each: start +0, end +0x10) in the mask; set a span's end to 0 when it adds nothing; return non-zero when any span was visible |
FUN_004ddeb5 | FUN_004dcdf0 (Ghidra's match by calls, 0.54) | The same for 0x14-byte span records, at a given row |
FUN_004ddf93 | FUN_004dcebc (match by calls, 0.66) | Test only: report whether any span is visible without marking |
FUN_004de02c | FUN_004dcf32 | Clear the pixels the mask leaves uncovered to the background, 8 pixels per mask byte, and reset the mask |
FUN_004de180 | FUN_004dd004 | Fill a rectangle of the 16-bit frame (movq stores, 128 bytes per loop) |
The two builds lay the mask out differently. The Pentium code works on 32-bit words with a table of 640 masks at 0x4fa798 (entry n has its top n bits set): pixel x is bit 31 − x mod 32 of word x / 32. The MMX code works on 64-bit words with an all-ones constant at 0x53ab18 shifted by psrlq: pixel x is bit 63 − x mod 64 of quadword x / 64. Nothing outside these routines reads the mask (inferred: the MMX callers FUN_00487d84, FUN_00488f08 and FUN_0049e13c only pass it on), and each clear routine reads its own layout.
Measured (2026-10-23, measure/mmx_coverage.py under Unicorn, 300 random span lists of 1 to 24 rows over random existing coverage, including empty and reversed spans): the two routines agree in all 300 cases on the returned flag, on which spans are marked hidden and on the set of covered pixels. The raw mask bytes differ in 299 of 300 cases (the bit layout), as expected. Output: derived/measure/coverage.json.
Textured spans (0x4de580–0x4df3c0)
| MMX build | Pentium build | Span kind (observed) |
|---|---|---|
FUN_004de580 (1,504 bytes) | FUN_004de3f0 (1,481 bytes) | Mode 0x60: textured, perspective-corrected through a table of 1/z at 0x5eed08, 16-bit pixels, writes aligned to 8 bytes |
FUN_004decf0 (1,745 bytes) | FUN_004def0d (1,394 bytes) | Mode 0x61: a second textured kind (inferred: lit) |
The dispatcher is FUN_00487d30 (MMX) and FUN_00497060 (Pentium): ebx & 99 picks the span kind. The MMX routines align the destination to 8 bytes and write whole 8-byte groups of four 16-bit pixels (observed); how they step the texture coordinates inside a group is not traced. Whether their pixels match the Pentium routines bit for bit is not measured (open: needs the 1/z table and a texture page set up in the emulator).
Measured: the mixer model
measure/mmx_mixer.py runs FUN_004d1460 under Unicorn on 500 random channels (random 16-bit samples, volume 0 to 63, pan 0 to 128, random existing mix) and compares the mix buffer with a model written from the instruction semantics (pmulhw, then paddsw), with the signed-byte volume rule above. All 500 match (measured, derived/measure/mixer-model.json). GameFrame's crates/mmx holds the same semantics as tested Rust.
The Pentium mixer FUN_004df88a uses a different formula (one double holding both sides' gains, rounded through the FPU; docs/systems/sound.md, "The mixer").
Measured: the two mixers
measure/sound_mmx.py mixes the same single channel (a random sine, pitch 1.0 or another step, looping or not, volume 1 to 127, any pan) into one cleared 880-byte block with WINPOD's FUN_004df88a and PODMMX's dispatcher FUN_004bfea4, 300 cases (measured, 2026-10-24, derived/measure/sound/mmx-vs-winpod.json):
- The two builds put the sides in opposite words: PODMMX's word 0 is
vol × (128 − pan), WINPOD'svol × pan. Compared with the sides swapped: - When the gain fits a signed word (
vol × max(pan, 128 − pan) × 4 ≤ 32767): the largest difference is 203, and 55% of the samples are within ±1. The rest is the range (127 against 128), the rounding (pmulhwtruncates; the FPU rounds and borrows across the sides) and the pipelining (WINPOD skips a sample per block and per loop). - When it does not, PODMMX's 16-bit gain wraps (
imulthenmovdof the low word) and that side comes out with the wrong sign and size: differences up to 32,389. Both builds compute channel volumes up to 126 the same way (FUN_004c70d8and its PODMMX twin), so loud sounds panned off centre come out wrong inPODMMX.EXE(inferred audible).
The MMX build is also a newer version of the library: its channel step is clamped to 0x200 when it runs past the loop, a loop keeps its overshoot when it wraps, and it has five reverb and flanger environments where WINPOD has one echo (observed in FUN_004bfea4 and FUN_004d1795). Un-POD follows WINPOD.
Open questions
- Pixel-exact comparison of the textured spans (
FUN_004de580againstFUN_004de3f0). - The channel kinds behind
+0x38and+0x3c(8-bit samples, stereo samples?).