Running the original
State: WINPOD.EXE (the software renderer) runs in a 640 × 480 window under Wine 11 on macOS (Apple silicon), reaches the main menu and plays its attract-mode demo race (measured, 2026-10-23). Not yet tried: driving a race with the keyboard, sound, CD audio, the intro videos (they need the Indeo codec from the disc's INSTALL folder), and the MMX and 3D-card builds.
run/wine/run.sh sets up the Wine prefix in derived/wine, makes a patched copy of WINPOD.EXE there with run/wine/patch_winpod.py, and starts it. The player's own file is never changed.
Setup
| What | Why | Evidence |
|---|---|---|
Drive D: is the disc's files, as a CD-ROM drive labelled POD2_0 | The game takes the first drive where GetDriveTypeA is 5 and the volume label is POD2_0, and opens data from it (FUN_00479954, the file-open wrapper FUN_0047ba70) | observed |
C:\windows\UbiSoft\Ubi.ini, section [POD2_0]: Directory=C:\POD\ (with the trailing backslash), Language, Configuration=1 | What the installer writes. The game appends Data\... to Directory without a separator; Configuration present turns on saving settings (DAT_004f6f1c) | observed, measured (without the backslash it looks for C:\PODData\...) |
Language=French (or German, Italian, Spanish) | This disc's DATA/BINARY/DICOS has no English folder; with English the game fails to open DIC_ERR.BMD and asks for the CD | measured |
Wine's Direct3D renderer=gdi | The software build only uses 2D DirectDraw; the OpenGL start-up under Rosetta crashed now and then | measured |
pod2_0\pod2_02.exe, which the game looks for at start, belongs to Ubi Soft's online updater ([osd] Valid): when the file is absent nothing happens (observed, FUN_00479954).
Patches
Each patch is a Deviation: from the original, made only so it runs under Wine on a modern display. Addresses are in WINPOD.EXE (1997-03-07).
| Address | Original | Patched | Why |
|---|---|---|---|
0x4a88b8 | FUN_004a88b8 reads the Cyrix configuration registers (ports 0x22/0x23, DIR0/DIR1 at 0xfe/0xff) from FUN_004a4284, the CPU check | Returns 0xff: not a Cyrix | Windows 95 let a program use those ports; Wine stops it with "privileged instruction" at 0x4a88cd |
DAT_004f7260 | 1: fullscreen | 0: windowed | The game runs in a window only on a 16-bit desktop and otherwise switches itself to fullscreen (0x4a73b1 on activation, 0x4a804b on WM_DISPLAYCHANGE) |
0x4a5699, 0x4a58b5 | Desktop depth = GetDeviceCaps(PLANES) × GetDeviceCaps(BITSPIXEL) into DAT_0069247c and DAT_00692480 | 16 | The windowed path (FUN_004a4810) sizes the window only when the desktop is 16-bit |
0x4a4740 | The EnumDisplayModes callback stores every mode (width, height, depth, 1; 16 bytes) in a table at 0x6920b8 with no limit | Keeps modes up to 1024 wide, at most 56 | A modern display lists more than the 60 modes that fit: the table overflows into DAT_0069247c, and the game asked for a 640 × 480 mode at 1,640 bits per pixel (measured with winedbg) |
0x4a3d12 | The back surface (DAT_00692098, 640 × 480, OFFSCREENPLAIN | SYSTEMMEMORY) has no pixel format, so it takes the screen's | RGB565 | The game draws 16-bit pixels; in a 32-bit surface each row showed twice, side by side |
0x4a3e60 | The game reads the pixel format of the screen (DAT_00692094, GetPixelFormat) into DAT_0060b314: 1 for RGB565 (green mask 0x7e0), 0 for mask 0xfc00, 2 for a blue mask 0x3f, else 3 (RGB555) | Reads it from the back surface | So the game draws RGB565; Wine's software blitter converts 565 to the 32-bit screen, but not 555 ("Failed to blit") |
The two replacement routines live in zero padding at the end of the code section (0x4e1440, 0x4e1470), checked to be unused zeros by the script.
More patches, made while trying to play a race (2026-10-23):
| Address | Original | Patched | Why |
|---|---|---|---|
PE header SizeOfStackReserve | 64 KB (0x10000); every thread the game creates with size 0 gets it | 1 MB | A small stack leaves no room for Wine's signal frame under Rosetta (inferred; it did not stop the failure below, but costs nothing) |
0x4a3f94, 0x4a457c, 0x4a7500, 0x4a8028; 0x4a45c9, 0x4a740b, 0x4a81dd | SuspendThread / ResumeThread on the game thread DAT_0060b274: the game pauses itself while its window is inactive, and at start on a 16-bit desktop (FUN_004a454c, FUN_004a459c) | Skipped (add esp, 4; xor eax, eax) | Wine on macOS suspends a thread with a signal; the game now keeps running in the background instead |
Driving it
run/wine/podinput.c (built with zig cc into the prefix, see run/wine/README.md) sends keys and clicks to POD from inside Wine with SendInput. Keys go as scancodes, which DirectInput reads, and nothing needs macOS's accessibility permission. Clicks work on Windows dialogs (the sound and CD driver questions). The game's own menu cursor moves by DirectInput's relative mouse movement, so a warped cursor does not move it.
POD_SOUND=0 runs without an audio device: the game asks what to do ("Echec d'initialisation du driver son") and "Ignorer" continues without sound effects. POD_DLLS=";mcicda=d" does the same for CD music.
Starting the Glide build in Windows 98 (x86Box)
On macOS, run/vm/x86box-glide.sh makes a copy of the Windows 98 base VM, creates a POD data CD labelled POD2_0, attaches it to that copy, and opens it in 86Box. Pass the path to the local gameframe-vms folder:
run/vm/x86box-glide.sh /Users/tanepiper/code/gameframe-vmsThe base disk in gameframe-vms/win98 is left untouched; the working disk and config live in derived/vm/win98-glide. The copied config enables 86Box's Voodoo Graphics card. In Windows, open the POD2_0 CD drive, run D:\SETUP.EXE, install POD, then start WPOD3DFX.EXE from the installed POD folder. The game executable itself selects Glide.
Measured: the copied Windows 98 image boots in 86Box; the generated, read-only ISO reports the volume label POD2_0. Starting the installer and the game in this guest has not yet been checked. The ISO contains POD data files only; target/pod/disc has no Red Book audio image, so this setup cannot verify CD music.
Starting the Glide build through the Wine wrapper
The POD 3dfx executable is WPOD3DFX.EXE. In a prepared VM with the wrapper DLLs and wrapper-pod.sh staged in ~/share, start it with:
run/vm/vm.sh start
run/vm/vm.sh ssh 'sh ~/share/wrapper-pod.sh start WPOD3DFX.EXE 2'The game runs on the VM's display :2. To view and control that display from the Mac, start VNC in the guest and forward its port over the VM's existing SSH connection:
run/vm/vm.sh ssh 'x11vnc -display :2 -rfbport 5902 -forever -shared -nopw -quiet -bg'
ssh -i derived/vm/key -o IdentitiesOnly=yes -p 2222 -N -L 5902:127.0.0.1:5902 pod@127.0.0.1Open vnc://127.0.0.1:5902 in a VNC client. Stop the game with run/vm/vm.sh ssh 'sh ~/share/wrapper-pod.sh stop'.
Every build in the emulated PC
run/vm/allbuilds.sh runs a smoke test of each of POD's 14 game builds through GameFrame's wrappers (run/vm/wrapper-pod.sh, the DLLs staged by run/wrappers/stage.sh, the in-memory patches in run/wrappers/pod-patches.txt). It covers the start, the menus, a single race on Beltane with the first car, and about 60 s of racing, with a capture at each fixed point, the DLL logs and the game's log. It writes one folder per build, copied to derived/vm/builds/. It runs two builds at once (LANES), each on its own display and Wine prefix. A full race is run/vm/pvr-race.sh (POD_EXE picks the build).
run/vm/vm.sh ssh 'sh ~/share/pod.sh setup; setsid nohup sh ~/share/allbuilds.sh > ~/allbuilds.out 2>&1 < /dev/null &'run/vm/e2e.sh drives a full race to the results screen on each of the 12 builds whose race globals are mapped in run/vm/podaddr.py. It records the race and saves startup, menu, race and results captures under ~/e2e/<build> in the VM. The two PowerVR builds remain in the smoke test; their race globals are not mapped yet. It refuses to replace an existing build folder, so choose a new OUT for another run.
cp run/vm/e2e.sh derived/vm/share/
rsync -a -e "ssh -i derived/vm/key -o IdentitiesOnly=yes -p 2222" derived/vm/share/ pod@127.0.0.1:share/
run/vm/vm.sh ssh 'setsid nohup sh ~/share/e2e.sh > ~/e2e.out 2>&1 < /dev/null &'Timings (2026-10-24, measured):
| Emulated PC | Run | Time |
|---|---|---|
| 4 CPUs, 4 GB | One full run (menus, then 4 minutes of racing; WPOD3DFX), one build at a time | about 11 min |
| 4 CPUs, 4 GB | Two 3D builds at once | ran out of memory (the PowerVR session's run, before sgl.dll stopped queueing frames) |
8 CPUs, 8 GB (vm.sh default since 2026-10-24) | One smoke run, two at once | 401 to 474 s per build (median 422 s) |
| 8 CPUs, 8 GB | All 14 builds, two at once | 54 min |
The smoke run was not timed on the 4-CPU machine: the batch was started straight after the restart. Most of a run is fixed waits sized for the slowest menus, about 1.5 frames a second for the 3D builds in software OpenGL. SLOW scales them.
Results of the first batch (derived/vm/builds/, 2026-10-24): 12 of the 14 builds reach the race on Beltane and draw it. These are WINPOD, PODMMX and PODMAX64 (software), WPOD3DFX, PODX3DFX and PODFXM64 (Glide), WPODATI and PODATIX (ATI), WPODS3D and PODS3DX (S3), and WPODPVR and PODPVRX (PowerVR). The two Direct3D builds (WPODD3D, PODD3DX) fault in their AVI player when Indeo is missing. WPODD3D reads address 8 at 0x47C5E5, in FUN_0047c3b0; PODD3DX faults at 0x48E648. With the intro off (POD_INTRO=0 writes PlayIntroAtStart=0 into Ubi.ini) they start into the game's demo race and draw it through Wine's Direct3D. They then fault at the same address when the next video plays (derived/vm/builds-d3d-nointro/). They need an Indeo codec (the Indeo session's stand-in).
Known problems
- A game thread dies under Wine 11.0 on Rosetta (measured, every run since the input tool, with or without sound and CD audio). About 15 seconds in, as the menu appears, Wine logs
virtual_setup_exception nested exception on signal stackfor the game thread: while delivering an exception to 32-bit code, Wine's signal handler faults inmemmove(libsystem_platform,_platform_memmove$VARIANT$Rosetta). The menu then stops: no cursor, no input, 0% CPU. A backtrace before the failure shows the game thread inFUN_004a4810(DirectDraw setup, afterEnumDisplayModes). One earlier run reached the demo race, so the timing varies. - Under Wine 11.18 (staging) the same fault shows its cause (measured,
+seh): a game thread faults atwow64cpu+0x1135with CS0x0107, the 32-bit code selector. The 32-to-64 transition of a system call happened without the mode switch, sowow64cpu's 64-bit entry ran as 32-bit code:xchg r14, rspbecamedec ecx; xchg esi, esp, ESP took the window handle0x20066, andmov edx, [rip + 0x2ec5]read address0x2ec5. Under 11.0, Wine then faults while building the exception frame on that ESP ("nested exception"); under 11.18 the exception is resumed and faults again, forever, and the menu freezes at 100% CPU. The 11.0 sound-thread crash (wow64cpu+0x1139) is the same fault. - It happens now and then, in game threads making system calls (
user32with the window handle;ReleaseMutexin the mixer loop). One run reached the demo race; most freeze at the menu. Tried without effect: no sound, no CD audio, a 1 MB stack, noSuspendThread, and not reloading FS and GS in the Watcom runtime (FUN_004d1a18,FUN_004d28cd,FUN_004d2925push and pop them). Inferred: a fault in Wine's 32-bit (WOW64) support on Apple silicon under Rosetta, not in POD.
Findings on the way
DAT_004f7260is the fullscreen flag: 1 fullscreen, 0 windowed;FUN_004a3eectoggles it, and the Alt-key handler near0x4a7c6dcalls the toggle (observed).DAT_0061ffacis the video mode (4: 640 × 480, 7: 512 × 384, 8: 640 × 400, 9: 800 × 600),DAT_0061ffae/DAT_0061ffafpick 8, 16 or 32 bits (observed,FUN_004a3eec).DAT_0060b314selects the game's 16-bit pixel layout; the colour interpolation inFUN_00492070has RGB565 paths (observed).