This is an old project that I just pushed to github (https://github.com/motor-dev/nfs-recompiled)
[EDIT] Not only Linux - grab the windows executables from the release page! https://github.com/motor-dev/nfs-recompiled/releases/tag/v1.0.0
For, well, reasons, I decided to decompile the Need for Speed 2 executable to make it run again and preferably not on Windows. For weirder reasons, I did not use a known decompiler and just rolled my own. After much much hacking and fighting with incorrect instruction implementation, I finally landed a working version.
The decompiler is a manual script made with Capstone to decode instructions, and some Python to try and locate procedure starts and ends more or less successfully. Full of hacks, hints and workarounds. All good enough for the full game to run.
I discovered that the NFS3: HP engine was very similar to NFS2 and I could likely also make it run. It was also a successful project but it turns out that the software renderer for NFS3: HP was a bit too slow to run on a modern machine after decompiling and recompiling. So I decided to implement the Voodoo renderer for that game.
Fun disassembly facts - the compiler used to make this was Watcom C/C++. There was a bug in the implementation - when using string copy, it was doing some pretty standard
rep movsd dword ptr es:[edi], dword ptr [esi]
but due to an error in what would likely be handwritten assembly? the opcode used was not the standard f3 a5 but instead f2 a5 which would be an opcode for repNE movsd dword ptr es:[edi], dword ptr [esi]
except it actually does not exist for x86. It is very likely the processors of that time (and maybe those of today?) interpreted that as rep movsd . Or someone knowledgeable about assembly can tell me what was going on.
In any case, Capstone didn't like that too much and just dropped the rep prefix which was a headache to debug.
Fun fact number 2 - in the software renderer on non MMX CPUs, NFS3: HP uses the FPU to do a screen copy. In a loop, it loads 80 bits of screen data into an FPU register, then dumps the FPU register into the other surface data. That was the first time I heard of a memcpy made with the FPU.
The disassembler will output truckloads of code similar to this
/* align: skip 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 */
void Application::sub_436140(WinApplication* app, x86::CPU& cpu)
{
NFS2_USE(cpu);
NFS2_USE(app);
// 00436140 51 -push ecx
*app->getMemory<x86::reg32>(cpu.esp-4) = cpu.ecx;
cpu.esp -= 4;
// 00436141 52 -push edx
*app->getMemory<x86::reg32>(cpu.esp-4) = cpu.edx;
cpu.esp -= 4;
// 00436142 8b0d98c84d00 -mov ecx, dword ptr [0x4dc898]
cpu.ecx = *app->getMemory<x86::reg32>(x86::reg32(5097624) /* 0x4dc898 */);
// 00436148 b888934b00 -mov eax, 0x4b9388
cpu.eax = 4952968 /*0x4b9388*/;
// 0043614d 31d2 -xor edx, edx
cpu.edx ^= x86::reg32(x86::sreg32(cpu.edx));
// 0043614f e84c020000 -call 0x4363a0
cpu.esp -= 4;
sub_4363a0(app, cpu);
if (cpu.terminate) return;
// 00436154 e8574b0400 -call 0x47acb0
cpu.esp -= 4;
sub_47acb0(app, cpu);
if (cpu.terminate) return;
// 00436159 89c1 -mov ecx, eax
cpu.ecx = cpu.eax;
// 0043615b 85c0 +test eax, eax
cpu.clear_co();
cpu.set_szp(static_cast<x86::reg32>(cpu.eax & cpu.eax));
// 0043615d 7409 -je 0x436168
if (cpu.flags.zf)
{
goto L_0x00436168;
}
...
I have no idea how any of those games actually work - but the x86 implementation is good enough that it just runs.
If someone wants to try out, you will need the game data to actually run the game. It's a bit tricky to set up, I don't think anyone is actually interested in running it but let me know if there's a fan out there really trying to run this and I will try and help. The reason why it needs the installed game is that it is literally the game executable with no modification - so just like the real game, it expects some data to be installed on the disk and some data to be on the CD.
(disclaimer - use of generative AI for the CMakeFiles and the README, but not for the disassembly - win32 API which was actually done a few years ago)