Videos (VIDEO/*.AVI)
POD's six videos: the Ubi Soft logo and the intro at start-up, the endings of a championship and the credits. They are Video for Windows AVI files with Intel Indeo Video Interactive 4.1 pictures (FourCC IV41). The game plays them through Windows' own AVI libraries, so it needs Intel's codec installed. Without it the game shows NoIndeo.Bbm ("Indeo not installed", stills.md) instead of each video.
State: the container is walked to the last byte on all 6 files (riff.py, crates/pod-video, measured), and every sound stream decodes (pod-video, equal to FFmpeg's output run as a black box, measured 2026-10-24). The pictures: Intel's Indeo 4.1 is specified below from Intel's own IR41_32.DLL (clean room), and POD's decoder (pod_video::indeo) gives all 13,663 pictures of the six videos byte-identical to Intel's codec run under the emulator (measured 2026-10-24). GameFrame's codec ir41_32.dll plays them through Video for Windows.
The files
| File | Picture | Rate | Pictures | Luma bands | Picture types | Sound |
|---|---|---|---|---|---|---|
LOGO.AVI | 640 × 480 | 29.97/s | 1 picture, then 59 empty chunks | 1 | intra | none |
INTROPOD.AVI | 320 × 208 | 25/s | 5,981 | 1 | intra, inter, bidirectional, non-reference, null | 5 streams: English, French, German, Italian, Spanish |
INTROCD.AVI | 320 × 208 | 25/s | 5,981 | 1 | as INTROPOD | 1 stream, PCM |
FINPOD.AVI | 320 × 240 | 25/s | 1,413 | 4 | intra | 1 stream, in one chunk |
FINPOD2.AVI | 320 × 240 | 30/s | 156 | 4 | intra, inter, bidirectional, non-reference, null | 1 stream |
CREDITS.AVI | 280 × 180 | 29.97/s | 131 | 4 | intra | none |
All measured from the headers (avih, strh, strf) and the picture headers of every frame. The video streams' strf say 24-bit; the handler in FINPOD.AVI's strh is written iv41, the others IV41. INTROPOD.AVI and INTROCD.AVI carry the same pictures (the same frame types in the same order); INTROPOD.AVI stores its intra pictures in 00db chunks. The INFO lists name Adobe Premiere 4 as the compressor.
The sound is Microsoft ADPCM (format tag 2) at 22,050 Hz, stereo, 1,024-byte blocks of 1,012 samples, with the 7 standard coefficient pairs in the format's extra data. INTROCD.AVI has 16-bit PCM at 22,050 Hz, stereo, instead. Measured from the strf chunks.
Where the game plays them
Observed in WINPOD.EXE (POD):
| When | Function | Videos |
|---|---|---|
Start-up, unless video is off (DAT_005829e0) | FUN_0049c73c | logo.AVI, then intropod.AVI |
| The end of a championship | FUN_0049c4c4 | finpod.AVI and credits.AVI when FUN_0049c494 is true, else finpod2.AVI (inferred: a won and a lost championship) |
Each goes through play_video (FUN_00460570):
indeo_installed(FUN_00460120) reads[Drivers32] VIDC.IV41fromSYSTEM.INIwithGetPrivateProfileStringA(defaultNo). The value must equalir41_32.dll, compared case by case (FUN_004c80a0,strcmp), andFindFirstFileAmust find<system folder>\IR41_32.DLL. OtherwiseFUN_00465250showsVIDEO\NoIndeo.BbmwithDrawDibDrawand the video is skipped. This is the "missing Indeo" message.open_video(FUN_004609c0) callsAVIFileInitandAVIFileOpenA. Forintropod.aviwith the languageJapaneseit opensintrojpn.avi(not on the disc);introcd.aviis kept as a second path (inferred: the fallback when the first does not open). When the file does not open it asks for the CD (FUN_004601e4, "Please insert POD CD-ROM."). It takes up to 25 streams withAVIFileGetStream. Each video stream getsAVIStreamGetFrameOpen(stream, NULL)(Windows chooses the output format) andDrawDibOpen; the sound stream is chosen by language from the table atDAT_004ea39c(inferred from the five named streams ofINTROPOD.AVI). The first frame's DIB gives the picture size.open_video_sound(FUN_004616c8) reads the stream's format withAVIStreamReadFormatand openswaveOutOpen(WAVE_MAPPER, format, window, CALLBACK_WINDOW)with it unchanged, so Windows' ACM converts the ADPCM. When the device is busy the video plays without sound ("Sound Driver Busy").FUN_00461fc4makes a memory DC and bitmap the picture's size and centres it in the window, or in 512 × 384 (DAT_0058218c).- The loop draws each frame (
FUN_004613ec, orFUN_00460ff4with sound) fromAVIStreamGetFrameat the sampleAVIStreamTimeToSamplegives for the clock, until the end or a key.FUN_004623b0closes everything.
What Intel's installer does
POD's INSTALL folder has Intel's setup IVI_95NT.EXE (InstallShield 3.00.083), IR41_32.DLL (Intel Indeo Video Interactive 32-bit driver, R4.11.15.91), IR32_32.DLL (Indeo 3, 3.24.15.3) and IV5_FLTR.EXE (an Indeo 5 package for DirectShow: IR50_32.DLL, IAC25_32.AX, IVFSRC.AX; no file on the disc uses Indeo 5). Observed in the setup's script: it installs ir41_32.dll, ir41_qc.dll, ir41_qcx.dll and iyvu9_32.dll, writes VIDC.IV41=ir41_32.dll under [drivers32] in SYSTEM.INI, and the value vidc.iv41 = ir41_32.dll (description "Indeo® video interactive R4.1 by Intel") under the Drivers32 registry key. That is what indeo_installed checks.
A stand-in codec
run/vm/indeo.sh installs a codec named ir41_32.dll in a Wine prefix as Intel's setup would (system folder, system.ini, registry); run/vm/pod.sh and run/vm/wrapper-pod.sh call it with POD_INDEO=1, and run/wrappers/stage.sh stages the DLL when GameFrame builds one. The codec is GameFrame's wrappers/ir41 ("The codec", below). A first one was withdrawn on 2026-10-24 because its decoder was not a clean-room implementation (AGENTS.md, rule 8). It showed what a codec must do to get past the game: with it installed, POD's WINPOD.EXE in the emulated PC played the logo and the intro with no NoIndeo picture.
POD's two Direct3D builds do not carry on without the codec: WPODD3D.EXE faults reading address 8 at 0x47C5E5 in its AVI player (FUN_0047c3b0), and PODD3DX.EXE at 0x48E648 (measured by the race-loop session, derived/vm/builds/). With an Indeo codec installed (the withdrawn one, then GameFrame's clean-room one), both play the intro and reach the menus: the crash is the missing codec (measured, 2026-10-24).
The pictures
Clean room: the format is derived from Intel's own codec, IR41_32.DLL R4.11.15.91 from POD's INSTALL folder (traced in Ghidra, and its DriverProc run under the emulator on POD's frames), from the AVI files, and from public prose descriptions of the format. No other project's source code is read; FFmpeg's ffmpeg program is only run as a black box to compare decoded pictures.
Ghidra's export of the DLL is analysis/export/indeo/IR41_32.DLL/ (ignored by git; made with scripts/ghidra-export.sh --project analysis/ghidra-indeo --out analysis/export/indeo). Addresses below are the DLL's (image base 0x10000000). Every item names the code or data it is read from. A table is cited by its address and never copied into POD.
The original decoder, run as ground truth
measure/indeo_emu.py runs Intel's codec under GameFrame's pe32_emu (measured 2026-10-24): the DLL's start-up and DllMain (entry, 0x1005B800), then DriverProc (0x100320B0) with DRV_LOAD, DRV_OPEN (an ICOPEN for vidc/IV41), ICM_DECOMPRESS_QUERY and ICM_DECOMPRESS_BEGIN, then ICM_DECOMPRESS on each video chunk in file order. Windows is stubbed: memory from the emulator's heap, an empty registry, a counter for the clock, a Pentium for GetSystemInfo. It writes each frame's Y, V and U planes.
- Output formats (
FUN_1002fe10, the decompression format check): 8-bit palette, 16-bit RGB (BI_RGB,BI_BITFIELDS,BITM), 24-bit RGB, andIF09at bit count 9, which is Intel's YVU9 planes. The harness asks forIF09, so the pictures come out as the decoder made them, before any colour conversion. (PlainYVU9is offered only to Intel's capture tool:FUN_10034ab0checks the registry keyHKEY_CLASSES_ROOT\cap32ivi.) - The emulated codec is the reference, not the
ffmpegprogram, which differs from it on many pictures ("Checked against Intel", below).
./gameframe python measure/indeo_emu.py target/pod/disc/INSTALL/IR41_32.DLL target/pod/disc/DATA/BINARY/VIDEO/CREDITS.AVI derived/video/intel/credits.yvu9 0 3Where the decoder is
| Address | What | How it is known |
|---|---|---|
| 0x100320B0 | DriverProc: driver and codec messages | observed, the export |
| 0x1002ED10, 0x1002FE10 | ICM_DECOMPRESS_QUERY; the input and output format checks | observed |
| 0x1002F110 | ICM_DECOMPRESS (message 0x400D) | observed |
| 0x10026690 | one chunk: the first picture, and a second when an intra picture has one after it | observed, measured |
| 0x1002C710 | the pictures from a point in the chunk: header, references, bands, output | observed, measured |
| 0x100208D0 | the picture header | observed |
| 0x1002C4C0, 0x1002C570, 0x1002C620 | the frame stores before and after a picture; the swap of the two picture contexts | observed, measured |
| 0x100188D0 | a band header | observed |
| 0x100239F0 | a band's tiles | observed |
| 0x10019540 | a tile's macroblocks, and (Pentium path) their motion compensation | observed, measured |
| 0x10001C90 | a tile's blocks: coefficients, DC prediction, transform, store (hand-written assembly) | observed, measured |
| 0x1000A520 | a band's decoding context: codebook, run/value map, scan, quantiser tables, routines | observed |
| 0x10025F10 | the shown frame to 8-bit planes | observed |
| 0x10001290 | start-up: picks the Pentium or MMX path, widens the 4 × 4 tables | observed |
Two paths: Pentium and MMX
At start-up FUN_10001290 takes the MMX path (DAT_1006A158 = 1) when CPUID reports MMX and the registry key HARDWARE\DESCRIPTION\System\FloatingPointProcessor opens; otherwise the Pentium path (0). The harness opens no keys, so Intel runs its Pentium path (measured). The two paths give the same chroma and the same one-band luma, but recompose four-band luma differently: on CREDITS.AVI 25,400 of 50,400 luma samples of picture 0 differ (measured, the registry stub made to open the key). This specification and POD's decoder follow the Pentium path. Under Wine on a modern PC, Intel's codec would take its MMX path (inferred).
Bits
Intel's reader keeps a 32-bit window and shifts it right, filling new bytes in at the top (FUN_100208d0): a field's first bit is the lowest bit of the next unread byte, and a field of n bits is the window's low n bits (observed). The block reader at 0x10001C90 reads the same order from 32-bit words.
The picture header
In order, as FUN_100208d0 reads it (observed):
| Bits | Field | Notes |
|---|---|---|
| 18 | start code | must be 0x3FFF8 (mask at 0x10064A60), else error 0x10 |
| 3 | picture kind | 0 intra; 1 to 4 predicted (see "The kinds of picture"); 5 and 6 null, the header ends here; 7 is an error |
| 2 | flags | bit 1 set is an error (0xF); bit 0 is kept at instance +0xA0 (transparency; 0 in POD) |
| 1 | data size present | then 24 bits: the picture's size, kept at instance +0x38. In POD every picture has it: the chunk's length less 2, or the intra picture's own size when a second picture follows it |
| 1 | locked | then a 32-bit key in two 16-bit halves; a locked picture without a key is error 0x14 |
| 3 | size | 0 to 6: height from 0x10064930 (480, 240, 120, 480, 240, 288, 144) and width from 0x10064908 (640, 320, 160, 704, 352, 352, 176); 7: 16 bits height, then 16 bits width. A size change in a picture that is not intra is error 9 |
| 1 | tiles | when set: 4 bits tile height, then 4 bits tile width, each (n + 1) × 32 from 0x10064D10, 15 meaning the picture's own size; when clear, one tile. A change outside an intra picture is error 0xF |
| 2 | chroma format | 0, 1, 2: chroma width divisor 4, 2, 2 (0x10064958) and height divisor 4, 2, 1 (0x10064978); 3: then 2 bits width divisor and 2 bits height divisor, 1 to 4 (0x10064968, 0x10064988). Plane sizes round up |
| 2 × n | luma bands | a tree of 2-bit codes, read until no node is open (one is open at the start). Code 0 halves the width, 1 halves the height, 2 halves both; they open 1, 1 and 3 more nodes (0x100648D8, 0x100648E8, 0x100648F8). Code 3 is a band: its size is the plane's shifted by the halvings on its path, and it undoes the halving of the node it ends. The node records are indexed by the count of open nodes, and the chroma walk reuses them |
| 2 × n | chroma bands | the same tree for the chroma planes |
| 1 + 20 | picture number | kept at instance +0x44 (0 when absent). A predicted picture whose number equals the last one's (+0x44, or +0x48 when the last was held back) becomes kind 5, null |
| 1 + 8 | skipped | |
| 1 (+ 3) | macroblock codebook | absent: Intel's descriptor 7 (0x10064AA0); else 3 bits, 0 to 6 a descriptor, 7 the stream's own: 4 bits n, then n rows of 4 bits |
| 1 (+ 3) | block codebook | the same, from the descriptors at 0x10064B40; bands may choose their own |
| 1 + 3 | run/value map | the map bands use unless they choose one; absent: map 8 |
| 1 | instance +0x9C | |
| 1 | quantiser for every macroblock | instance +0x98: the luma band 0 reads a quantiser delta for every macroblock, the empty ones too |
| 5 | instance +0x4C | |
| 1 + 3 | held back | instance +0x94: non-zero for a picture decoded now and shown at the next anchor |
| 1 + 16 | part of a locked picture's check | |
| 1 (+ 8 + 1 …) | extension | while the bit is set, 8 bits (kept at +0x3C) and another bit |
| 1 | when set, one bit for each 64 × 64 square of the picture, skipped |
The header ends at the next byte. Every field of POD's pictures was read this way on all 13,663 pictures (measured: the decoder reads each one to its end).
Intel's tables
They are Intel's data: POD reads them from the player's own IR41_32.DLL (pod_video::indeo::Tables::from_dll), never copies them.
| Address | What |
|---|---|
| 0x10064AA0, 0x10064B40 | 8 macroblock and 8 block codebook descriptors, 17 bytes each: a row count, then each row's bits |
| 0x100765DC + 0x14C × n | 9 run/value maps: +1 the number of levels of each run, from run 0 to the first 0; +0x48 a permutation of the 256 symbols |
| 0x100A44F8 + 64 × n | 7 scan orders, positions in an 8-wide block (a 4 × 4 scan stored 4-wide, see below) |
| 0x100784F8 | quantiser steps, 16 bits: ((matrix × 2 + set) × 32 + q) × 64 + position, 22 matrices to 0x100A44F8; set 1 is for intra blocks |
| 0x1006A178 + 0x38 × n | the transform descriptors: three routines, a name ("Inv Slant 8x8", "Inv Haar 1x8", "No Xfrm 8x8", …), flags at +0x34, data at +0x30 |
At start-up Intel widens the 4 × 4 tables to the 8-wide layout (observed, measured): FUN_10001400 changes each position p of a 4 × 4 scan (one whose last two bytes are equal) to p + (p & 0xC), and FUN_10001440 moves the steps of each 4 × 4 matrix (one whose first table's step 63 is 0) the same way, table by table, from the last step down. Without it the chroma of LOGO.AVI came out wrong (measured).
Codebooks
FUN_100016e0 builds a lookup table from a descriptor (observed). Row i's codes are i one bits, a zero bit (the last row has none), then the row's bits with the most significant first. Its symbols follow the rows before it, one for each value of the row's bits. A row of more than 8 bits, or a longest code over 13 bits, is refused. The descriptor of one row of 8 bits is a special case: its table is the identity, so the symbol is the 8 bits read in the stream's own order.
Macroblock codebook symbols stand for signed values through the table at 0x100AFE80, which start-up fills (measured): 0, +1, −1, +2, −2, …, each biased by 0x80.
The band header
In order, as FUN_100188d0 reads it (observed):
| Bits | Field |
|---|---|
| 2, 4 | plane and band number |
| 1 | empty: the band has nothing more (flag +0x30 bit 0); its header is one byte |
| 1 + 16 | band +0x18 |
| 2 | band +0x1C: the motion vectors' resolution, 0 whole samples, else halves |
| 1 + 16 | band +0x20 (−1 when absent) |
| 2 | the macroblock size: 16, 8, 4 (0x10064820); 3 is an error |
| 1 | the band takes its motion vectors from the luma band 0's macroblocks (+0x2C) |
| 1 | the band takes its quantiser deltas from them (+0x28) |
| 5 | the band's quantiser, 0 to 31 (+0x34) |
| 1 | when 0: 5 bits transform (+0x38), 4 bits scan (+0x39; 15 is a scan in the stream), 5 bits quantiser matrix (+0x3B; 31 is a matrix in the stream). When 1, the band keeps the last ones (its structure is zeroed when the layout changes) |
| 1 (+ 3) | block codebook: absent, the picture's; else as in the picture header |
| 1 + 3 | run/value map; absent, the picture's |
| 1 + 8 + 16 × n | n (at most 61) pairs of symbols whose runs and levels swap |
Then the next byte. POD's bands: one luma band with 16 × 16 macroblocks, transform 4 (Slant 8 × 8), scan 0, matrix 13; or four luma bands with 8 × 8 macroblocks, transforms 0, 1, 2, 3, scans 1, 3, 2, 4, matrices 1, 5, 9, 12; and chroma with 4 × 4 macroblocks, transform 11 (Slant 4 × 4), scan 5, matrix 15 (measured, the band structures in the emulator).
Tiles
A band has the picture's tiles, scaled to the band (FUN_100208d0, FUN_100239f0, observed). Each tile starts with a bit: 1 is an empty tile, which a predicted picture (kinds 2 to 4) copies from the forward reference; the next tile's bit follows at once. Else a bit says whether 8 bits of size follow (255 means 24 bits follow), and the tile's data starts at the next byte: its macroblocks, then from the next byte its blocks. The size counts from the tile's first byte to the end of its blocks, and the next tile starts there.
A band that was empty since the last intra picture (+0x30 bits 0 or 1 set) is filled with a constant before it is decoded again, and its empty tiles are not copied (FUN_10018810).
Macroblocks
FUN_10019540 reads a tile's macroblocks in rows (observed, checked against Intel's block lists in the emulator):
- One bit: 1 is an empty macroblock. In an intra picture it is intra, with no coefficients; in a predicted picture (kinds 2 to 4) it is forward with a zero vector, a copy of the reference (0x10064858 mapped through 0x10077210). The luma band 0 of a picture with flag +0x98 then reads a quantiser delta.
- Else: the macroblock's type, 1 bit in kinds 2 and 4 and 2 bits in kind 3 (0x10064870), none in intra pictures or bands that take their vectors from luma: 0 intra, 1 forward, 2 backward, 3 both (0x10077210).
- The coded-block pattern: 4 bits for a 16 × 16 macroblock (its 8 × 8 blocks top-left, top-right, bottom-left, bottom-right, from the low bit), else 1 bit.
- A quantiser delta (a macroblock codebook symbol) when any block is coded, or always in the luma band 0 with +0x98. A band that takes its deltas from luma uses the luma macroblock's, clamped so the sum stays in 0 to 31. Intra blocks use the intra set of tables.
- For a moving macroblock that reads its own: two symbols, the vertical then the horizontal difference from the last vector read in the tile (it starts at zero). For both directions, two more for the second vector, a difference from the first; the next prediction is the second. A band that takes its vectors from luma halves or quarters them for a smaller macroblock, on the biased bytes.
The blocks are listed in the tile's order. A row with an odd number of blocks ends with an uncoded intra block, so that blocks always pair with their right-hand neighbour (below).
Blocks
The band's context (FUN_1000a520) expands the run/value map (FUN_100015d0, observed): the symbols the permutation sends to 0 and 1 end a block and escape. Then, for each run from 0, its levels −n … −1 and +1 … +n take the next symbols. The band's pairs of symbols swap their runs and levels.
A coded block's coefficients follow in turn (0x10002462, observed):
- A symbol from the block codebook. An ordinary symbol moves the scan position on by its run + 1 and adds its level. An escape is followed by three more symbols: the run, then the level's low and high parts, level = high × 64 | low (0x10002A47). An end of block ends the block. It is seen only through Intel's fast table, which holds codes of up to 10 bits (
FUN_100019f0). - A level ±k at step s is ±(k·s + ⌊s/2⌋ − 1 if s is odd and over 1) (
FUN_100014e0builds these tables for steps 1 to 512). The step is the band's matrix at the quantiser and scan position (FUN_10001050). - An intra block's DC is coded as a difference from the last intra block's, in the tile's order (0x1000221E). The predictor starts at 0 in each tile, and uncoded intra blocks take it.
Intel's Pentium path works on pairs of horizontally adjacent blocks. The coefficient buffer is 8 rows of 9 words, and each 32-bit word holds the left block's value in its low half and the right block's in its high half. The words start at 0x08000800 (Slant) or 0x40004000 (Haar, no transform; descriptor data +4), and the levels are added to them. The transforms are Intel's word arithmetic on such pairs. POD's pod_video::indeo ports each routine word for word; 2,400 buffers through each of the six against Intel's own routines are bit-exact (measure/indeo_vectors.py, crates/pod-video/tests/indeo_transforms.rs, measured):
| Transform | Routine | Uncoded intra block (0x10016780 …) |
|---|---|---|
| 0 Haar 8 × 8 | 0x10015310 | every sample 0x4000 + ((DC & ~7) >> 1) |
| 1 Haar 1 × 8 | 0x10015230 | row 0 0x4000 + (DC & ~3), the rest 0x4000 |
| 2 Haar 8 × 1 | 0x10015130 | column 0 0x4000 + (DC & ~3), the rest 0x4000 |
| 3 no transform | 0x100155D0 (samples × 4 + 0x4000) | sample (0, 0) 0x4000 + 4·DC, the rest 0x4000 |
| 4 Slant 8 × 8 | 0x10009B60 | every sample 0x4000 + ((2·DC + 2) & ~3) |
| 11 Slant 4 × 4 | 0x10009ED0 | the same |
A band sample is 16 bits, 0x4000 + 4 × the pixel's offset from 128 (for one band), always a multiple of 4. An intra block replaces the band's samples; a predicted block adds out + band − 0x4000 (0x10015660 for 8 × 8, 0x10015A60 for 4 × 4). Where one block of a pair is intra and the other predicted, Intel adds whole words and then overwrites one half, so a carry between the halves falls as it does in the original; POD keeps these word sums.
In memory a band's words hold samples x and x + 8 of each group of 16 (x and x + 4 of each 8 for 4 × 4 blocks). Only the four-band output depends on that layout.
Motion compensation
The Pentium path predicts each moving macroblock as it reads it (0x1001E0E6, the fragments its tables at 0x10077220 and 0x10077520 name; observed, checked on FINPOD2.AVI):
- A vector's low byte is horizontal and its high byte vertical, both biased by 0x80. With whole-sample vectors (band +0x1C = 0) they are samples. Otherwise the vertical is halved with an arithmetic shift (the carry is the half bit), and the horizontal's low bit is the half bit.
- A forward macroblock predicts from the forward reference (instance +0x7C). A backward one predicts from the backward reference (+0x80) at minus its vector. One with both predicts forward with its low vector, then averages in the backward reference at minus its high vector.
- The first prediction of a sample: whole, the reference sample; half horizontal
((a >> 1) + (b >> 1)) & 0x7FFC; half vertical((a + c) >> 1) & 0x7FFC; both(((a + c) >> 2) + ((b + d) >> 2)) & 0x7FFC. - The second, averaged with the first p: whole
((p + r) & 0xFFF8) >> 1; half horizontal((a >> 2) + (b >> 2) + (p >> 1)) & 0x7FFC; half vertical((p >> 1) + ((a + c) >> 2)) & 0x7FFC; both((((a + c) >> 2) + ((b + d) >> 2) + p) & 0xFFF8) >> 1.
The prediction is written into the picture's frame store, and the coded blocks add to it.
Output
FUN_10025f10 writes the shown frame store to 8-bit planes (observed, measured). One band: pixel = clamp((sample >> 2) − 0xF80, 0, 255) through the table at 0x10077070 (valid from index 0xB80 to 0x117F; outside it the table reads other data). Four bands (FUN_10024eb0): the inverse Haar step on the words of the four bands at the same place. With a = LL, b, c, d the others in the header's order, Intel forms in 32 bits i3 = a − 0x77F777F8 − b, i7 = c − d, u4 = i3 − i7, i6 = i7 + 0x7FFF8000 + 2d, u5 = u4 + 2·i7, u12 = i3 + 0x7FFF8000 + 2b − i6 and u10 = u12 + 2·i6. Each gives a pixel of the 2 × 2 block (u10, u12 on the top row, u5, u4 below) as (v >> 4) in each half, through the clip table at 0x100779F0 when bits 8 or 9 of either half are set. The chroma planes come out at a quarter of the width and height, rounded up (IF09: Y, then V from plane 1, then U from plane 2).
The kinds of picture and the frame stores
Intel keeps three frame stores (instance +0x78 receives, +0x7C the forward reference, +0x80 the backward reference) and the one to show (+0x84), with a flag for each (+0x68 … +0x74), and two picture contexts that FUN_1002c620 swaps (observed, measured on FINPOD2.AVI):
- Before a picture (
FUN_1002c4c0): a held-back picture with a backward reference first makes that the forward one. An intra picture swaps the receiving store and the forward reference. - After it (
FUN_1002c570): a held-back picture shows the forward reference and becomes the backward one. Intra and kinds 1 and 2 swap the receiving store into the forward reference and show it. Kinds 3 (bidirectional) and 4 show what they received and are never references. - Kind 5 shows nothing new. Kind 6 swaps the contexts and shows the held-back picture.
- A chunk whose intra picture does not fill it, and is followed by a byte 0xF8 (the start of another picture), holds a second picture:
FUN_10026690decodes it in the other context and shows nothing for it.
POD's predicted videos (the intros, FINPOD2.AVI) repeat the chunk pattern I(+P) B B P B B P B B P B B, then a null 6 and two droppable P (kind 4): the P after the intra is decoded with it and shown three chunks later (measured).
Checked against Intel
POD's decoder (crates/pod-video, pod_video::indeo) gives every picture of POD's six videos byte-identical to Intel's codec under the emulator (measured, 2026-10-24, crates/pod-video/tests/indeo_pod.rs):
| File | Pictures | Equal to Intel's | Equal to the ffmpeg program's |
|---|---|---|---|
LOGO.AVI | 1 | 1 | 1 |
CREDITS.AVI | 131 | 131 | 0 (chroma in all 131) |
FINPOD.AVI | 1,413 | 1,413 | 10 (chroma in all) |
FINPOD2.AVI | 156 | 156 | 0 |
INTROPOD.AVI | 5,981 | 5,981 | 3,759 |
INTROCD.AVI | 5,981 | 5,981 | not measured |
The ffmpeg program was run as a black box. It differs from Intel's codec on every four-band luma picture and on many predicted ones; Intel's emulated codec is the reference.
What the decoder does not do, because POD's videos do not use it, each an error: transforms other than 0 to 4 and 11, scans and matrices in the stream, transparency, locked pictures. Where Intel reads memory outside its tables or bands (a scan position past 63, a reference outside the band, a sample outside the clip window), the decoder does something defined instead; each is marked Deviation: in the code. None happens in POD's videos.
The codec
GameFrame's wrappers/ir41 (ir41_32.dll, GameFrame PR 20) is a Video for Windows codec on the same decoder (GameFrame's crates/indeo). It reads Intel's tables from the player's own DLL: WRAPPER_IR41_TABLES, else ir41_32.intel.dll beside it, else X:\INSTALL\IR41_32.DLL on a CD drive. run/wrappers/stage.sh stages both files and run/vm/indeo.sh installs them as Intel's setup did. Under Wine, Windows' AVIStreamGetFrame through the codec gave all 156 pictures of FINPOD2.AVI, the IF09 ones byte-identical to Intel's (measured). Its RGB pictures use the BT.601 equations, not Intel's own colour conversion.
In the emulated PC (POD_INDEO=1, measured by the race-loop session, 2026-10-24): POD's WINPOD.EXE plays the intro through the codec, with no NoIndeo picture in 25 captures (derived/video/vm/); the codec's log shows the tables read and three decompressions to 24-bit RGB. WPODD3D.EXE and PODD3DX.EXE no longer fault in their AVI player: both play the intro, run the demo race and reach the main menu and the car screen (derived/vm/builds-d3d-cleanroom/).
How the clean room was kept
The user's rule (AGENTS.md, rule 8). The first decoder, written from FFmpeg's compiled library, was withdrawn on 2026-10-24. This one was written by a session that had not seen it, from: Ghidra's export of IR41_32.DLL (analysis/export/indeo/), its disassembly (objdump), and Intel's own routines run under pe32_emu: whole pictures (measure/indeo_emu.py), single routines on chosen inputs (measure/indeo_vectors.py), and Intel's internal block lists and band buffers read from the emulator to find where the decoder first went wrong. The six transforms were translated from Intel's code by a small tool and fuzzed against the originals. No other project's source, data or description of its internals was read; the ffmpeg program was only run to compare output.