A matching decompilation recovers C source code that, when compiled with the original compiler, produces the same machine code as the shipped game. It is slow, exacting work, which makes it a good test of whether automated workers can do precise work that is checked rather than trusted. To find out, I designed a system of agents to do it: one coordinating agent that plans the work and hands it out to many others, including agents from a different vendor, while I set the rules and make the calls that matter.
The target is Resident Evil 3: Nemesis, Capcom’s 1999 survival horror game, in its Japanese PC release from SourceNext (version 1.1.0). Its main executable is about a megabyte of x86 code, built by a Microsoft compiler that was already old when the game came out. This is the story of the project so far, told from its own records: 345 commits, 37 decision records, a recipe library of more than 2,600 lines, and an evidence note for every function. It includes the parts that went wrong, because the records do.
How it works
- Worker agents each lease a function from the original binary and propose C source for it.
- A single coordinator compiles each proposal with the original toolchain, Microsoft Visual C++ 6, and compares the result with the shipped code.
- A function is credited only after a full replay matches the original executable. Most matches are exact; the rest match once addresses that are only fixed at link time are normalised. Anything else is rejected.
Nothing is accepted on trust: the compiler is the judge.
In practice a single function’s record reads like this. The address and numbers below are made up for this page:
function 0x4A10C0
attempt 1 compiles; 61 of 64 bytes match rejected (register order differs)
attempt 2 compiles; 64 of 64 bytes match staged
replay whole executable rebuilt; matches credited
A near miss counts for nothing. Closing one is usually a matter of writing the code the way the original programmers probably did, because the old compiler turns small differences in style into different bytes.
Work runs in waves. Workers stage attempts, the coordinator admits, replays and credits them, and each accepted wave is a checkpoint with a reproducible report. There have been 174 of them so far.
How the work was organised
The “coordinator” in that pipeline is itself an agent, a long-running Claude Code session that I set up to orchestrate everything else. It reads the project’s state, decides what the next wave should attempt, writes a brief for each piece of work and starts worker agents to do it. When they report back, it checks what they found against the compiler and decides what is admitted. My own job is to set the rules, review the decision records, and answer the questions that are mine to answer.
Every worker starts with the same preamble. It may read anything, but it works only in its own scratch folder. It may not edit a tracked file, touch the original game files or run git. Only the coordinator changes the repository, admits a match or credits a function. That one rule is what makes it safe to run many workers at once: a worker can be wrong, or even make something up, and the worst outcome is a rejected attempt in a folder nobody else reads. Over the project there have been more than 800 of these worker runs.
More than one kind of agent
The workers were not all the same. Most were Claude subagents started by the coordinator. When the fleet hit the limit of 20 agents running at once, the coordinator began handing work to Codex, OpenAI’s coding agent, by running it from the command line with the same preamble and the same scratch-folder rules. Pools of Codex workers reviewed 175 near misses in one stretch and staged six new matches worth 3,367 bytes, all of which went through the same compiler check as everyone else’s. The compiler does not care who wrote the code.
Mixing vendors had its own lessons. One emulation job was stopped by Codex’s content filter partway through, and a Claude worker picked it up and finished it. Alongside the agents ran plain programs: an unattended orchestrator that drove “permuters”, tools that try thousands of small rewrites of a near miss and keep any that score better. Between them they ran more than 9.2 million compiles. Windows Defender had been rescanning every one of them, which an exclusion fixed.
When the system slipped
The coordinator made mistakes, and the records keep them.
- A launch before its driver existed. The permuter orchestrator once started a batch while a worker was still building the program meant to drive it. It launched 181 processes with that file missing. All of them exited before compiling anything, nothing they touched was trusted, and the incident was written up like any other.
- A race in admission. Workers kept staging new matches while an earlier batch was under review, so a late arrival could slip into an admission nobody had reviewed. The fix,
admit --only, makes each admission name exactly the addresses that were reviewed and leaves anything later for the next batch. - Review slips. Twice a review described a function’s behaviour wrongly, once for
0x00402830, where it left out nine indirect calls. The bytes and the match were never affected, but the claims were wrong, so both were corrected in later commits and kept in the record.
Who gets to say yes
Some decisions belong to me, and the system was built to keep it that way. When the port first started, it stopped at the game’s own installation check. The coordinator’s next step was to bypass that check, and Claude Code’s permission system blocked it as a change that weakens security. The coordinator brought the question to me instead of finding a way round. Along the way it relayed an approval to a worker on my behalf, and the worker declined to act on it, because a message from another agent is not the owner’s consent. I approved the change in chat myself, and only then was it made.
Budget was a decision too. The agents run on a weekly usage allowance, so the coordinator planned waves around what was left and wound workers down cleanly instead of leaving work half done. Late one night I decided that usage mattered more than CPU time, and the permuters and their orchestrator were paused.
Ground rules, before any code
The first decisions were about what the project would never do. The installed game is kept as a preserved master that nothing runs, patches or reformats; every experiment works on a copy. The repository admits only reviewed source, tools, tests and short metadata, through explicit allowlists. Game files, assets, saves, compiler installations and bulk disassembly stay out of it. No game code or data is published, then or now.
Decompiled pseudocode is treated as a hypothesis, not as source. Every recovered function needs an address, a prototype with evidence for its calling convention, a confidence level, a verification status and a list of open questions. A field seen at some offset can justify a small view of that memory; it does not justify inventing a whole structure. Names stay provisional until something in the binary backs them up.
Then the compiler. The executable’s linker header says version 6.0, which is a lead, not proof. Its Rich header, a block of build metadata the Microsoft linker leaves in every executable, lists 15 records: mostly objects from Visual C++ 6 Service Pack 5 (build 8966), some from an older build (8447), and a few from later tools that probably came in with libraries. I installed the command-line compiler from a German Visual C++ 6 disc, build 8168, and pinned it. That gap between the compiler I had and the compilers the metadata names comes back several times in this story.
First matches, and a pass that produced nothing
The first recorded progress was eleven functions: 123 bytes out of a 1,096,778-byte code section, 0.01%. Six matched exactly; five matched once their address operands were checked and normalised.
The first attempt to speed things up was an instructive failure. Six workers prepared hypotheses covering 1,876 bytes, but none could be compiled into a valid comparison until some object-file details were settled. The one that did compile, the startup function at 0x00401000, came out at 661 bytes instead of 672, with differences all through it. No amount of editing metadata could turn that into a match. The pass ended with zero new verified bytes, and the handoff notes said so plainly instead of counting prepared sources as progress.
Small functions are easier, so the function count rose far faster than the byte count. By wave 25 the project had 1,567 functions but only 139,680 bytes, 12.7% of the code section.
When the process was the bottleneck
The early pipeline was careful to the point of paralysis. Separate approvals for the source, the compiler run, the leased inputs and the handoff meant that, at one point, roughly 44 bespoke scripts were being written for every function admitted. Two agents sharing one checkout made coordination its own engineering problem: decision 0010 gave a single coordinator every tracked edit and git write, with the other lane working on a disjoint range and a handshake to pass control. When work was interrupted mid-wave, the record says exactly what was and was not accepted, and an unaccepted report with 1,608 rows was never quoted as coverage.
A process review found the tools healthy and the ceremony expensive. Decision 0011 replaced it with one driver, wave.py. Workers run next → native → try → stage; the coordinator runs acquire → admit → replay → credit → publish. Each function’s registry entry became one JSON file. Leases expire after two hours, attempts are logged append-only, and each function gets three distinct source attempts before a retry has to give a concrete new reason. A reviewer can block a batch only on a concrete defect: copied code, a wrong boundary, a gap in provenance, or a misleading claim.
The effect was immediate. Waves 35 to 46 took the total from 157,736 to 397,706 bytes, from 14% of the code section to 36%. Speed also exposed small defects in the pipeline itself: released leases that silently vanished from selection, an interrupted empty attempt that blocked the next one, 72 evidence notes missing their standard fields. Each was fixed and recorded.
Opening the gates
By wave 46 the easy candidates were gone, and much of what remained was code the comparator could not yet judge. Phase two (decision 0012) widened the checks one evidenced case at a time, never by relaxing them in general:
- Field offsets. About 25 near misses referred to a field inside a known global. The rule now allows exactly that compiled offset, checked against the original operand, and nothing else.
- Switch statements. Jump tables, index tables and their padding are reviewed as one envelope and replayed exactly. Only the code is credited, never the tables. A later survey fix found 39 switch functions, 46,560 bytes, that had been hiding behind a scanning bug.
- Exception handling and C++ classes. Unwind tables, compiler-generated vtables, deleting destructors and adjustor thunks are compared as complete graphs and reserved as zero-credit attachments.
- A second compiler. With the original’s metadata pointing at Service Pack 5, I obtained it from Microsoft’s signed installer and pinned it separately. Decision 0022 allows it for a function only after the same source has failed under the base compiler, and a match under it is evidence, not proof of how the original was built.
- A broader search. The candidate survey learned to find switch roots, functions reached only through pointers, tail calls and tiny stubs. Each new rule has a switch that reproduces the old output exactly.
The results show in the accepted waves: past half of the code section at wave 51, nearly two thirds by wave 58.
Going backwards on purpose
At wave 66 the percentage went down, deliberately. A library matcher identified 488 regions, 102,451 bytes, as code from Microsoft’s C runtime, DirectInput and the Independent JPEG Group’s libjpeg 6a, by rebuilding those libraries and matching them byte for byte. That is real, verified code, but it is not recovered game source, so it now counts in its own category with zero credit. Twenty-eight functions that had already been credited turned out to be library code and were retired. The wave added a function and still reported a net loss of 1,200 bytes.
I then asked for the coverage numbers to be as accurate as possible. Decision 0025 keeps the original measure, so that old and new figures stay comparable, and adds a conservative second one that removes only what is proved not to be code: 28,425 bytes of padding, 8,928 of jump tables, 1,757 of index tables and 4,800 of library data. Anything not proved non-code still counts as code.
That is why this page gives more than one number, and they must not be added together:
| Measure | What it counts | At wave 174 |
|---|---|---|
| Code section | Credited bytes of the whole 1,096,778-byte code section, including the runtime libraries the game links | 86.3% |
| Game address range | Credited bytes of the game’s own address range, padding and tables included | 95.5% |
| Game code | Credited bytes of the game’s own code, with proven non-code removed | 99.2% |
| Library code | Verified library code, with no recovery credit | 92.3% of the runtime’s code |
Does the recovered code actually run?
A byte match proves code generation. It does not prove that the recovered functions work together, so integration is tracked as a separate set of milestones.
The first step placed every credited body back at its original address in a copy of the executable and required the result to be identical to the original. That checks the harness, not the code, because an identical image runs identically. A placement linker then built the image from a blank template, resolving all 26,679 relocations by symbol name from the reviewed registry. It reproduced the original even from a deliberately corrupted template.
The first build that really differed moved 2,997 recovered functions into a new code section and left forwarding stubs at the old addresses. Most PC players run the game through Classic REbirth, a community compatibility patch that rewrites parts of the executable in memory, which complicated things: some functions had to stay where the patch expects them. The relocated build reached the main menu and intro movie, and stack samples showed the moved copies executing. The next one moved 3,239 functions and played through the opening scenes, the first gameplay room, a death and the return to the title, with screenshots matching the original.
Recompiling the same sources with different optimisation settings found a real bug. One function kept a one-byte local that a callee wrote four bytes into. The original’s stack frame happened to tolerate it; a smaller frame crashed. The credited source stayed as it was, because it is byte-exact, and three similar hazards were recorded. Running original and recompiled functions side by side in an x86 emulator, 500 random states each, found two more: one function with its first two pointer arguments reversed, and one that processed four slots where the original processed three.
Later, every function without a match yet was given its best near-miss source, and the whole game was built from source. I played that build from a save in the final area through to the ending, and from a new game up to the warehouse, without problems. That is one player’s observation of provisional code, not credit, but it changed the question from “can it be done” to “how complete and how clean”. A later build linked by the original Visual C++ linker took only 14,086 bytes of code from the original, most of them kept only for Classic REbirth. Everything outside the code, from data tables to resources, can be generated by tools that read the player’s own copy of the game, check its hash and write into an untracked build folder (decision 0028). Nothing extracted ever enters git.
The game also ships a second executable for its Mercenaries minigame. Comparing the two showed it is the same build with a single #define: one #ifdef block, one extra 80-byte function, three functions the linker folded differently, and otherwise only shifted addresses. Every shared function replays against it too, reported separately and never added to the main credit.
The craft: persuading a 1999 compiler
Once a function does the right thing, the remaining difference is usually register allocation, instruction order or stack layout. The compiler makes those choices across the whole function, so a change to one line can fix the first mismatch and create ten new ones elsewhere. Workers compare the complete function after every change, and everything they learn goes into a shared recipe library, including the things that did not work.
Some entries read like folklore until you see the evidence:
- In one function, the order of a three-term dot product was decided by the initialisation order of pointer locals, not by the order of the arithmetic.
- Which floating-point temporaries stay on the x87 stack depended on the order the vertices were stored.
- The compiler decides whether to keep a constant in a register by counting its uses as written, before it removes duplicate code, so a use far away could fix a register choice here.
- Globals held in registers are reloaded after a call in the order they first appear in the source, so a few statements that do nothing at the top of a function set that order.
- The compiler switches strategy at a function-size threshold that counts dead code, so a few dead expressions can flip an evaluation order.
The most useful single discovery was the constant-false operand:
if (state == 3 || (cursor && 0)) {
/* ... */
}
cursor && 0 is always false, so it emits no code. But the compiler counts it as a use of cursor before it optimises it away, and that is enough to change which register cursor gets. Within hours of finding it on one function, workers had used variations of it to fix spill choices, keep a constant in a callee-saved register, stop two loop pointers being merged and block a 16-bit narrowing. The library records its limits as carefully as its uses: x & 0, x * 0 and 0 && x are folded too early to have any effect, and in some positions it emits real code.
A recipe is a description of how this compiler behaves, not a claim about what the original programmers wrote. Where a match relies on such a device, the source says so in a comment.
When guessing stopped working
As the remaining functions got harder, several approaches were tried and measured, and most of what they produced was negative results. They are kept, because they stop the next attempt repeating them.
- Allocator experiments. 3,109 synthetic test functions produced rules that predicted held-out cases very well, 120 out of 120 in some families, and then completed zero of the thirteen real near misses they were aimed at. Later rounds found nine repairs, worth 2,982 bytes.
- Translation-unit context. Another decompilation of a game from the same era had found that surrounding code can steer an older Microsoft compiler. Across 1,782 comparisons on this one, unused declarations, names, file paths, pragmas and neighbouring functions changed nothing, apart from one effect already known.
- Brute force. Source permuters ran more than nine million compilations of random and guided variants. They found nine winners, 4,358 bytes, each then minimised by hand until the surviving edit could be explained. One needed only a change in the order two fields were read, found after 57 probes.
- Proving equivalence instead. Static equivalence checkers and differential emulation tried to show that a near miss behaves the same as the original even if its bytes differ. The checkers were conservative enough to be of limited use, and a reviewer built a false positive that exposed a real flaw in one of them, which was fixed. The emulator was better at finding bugs than at proving their absence.
What finally worked was measuring the compiler’s back end instead of guessing at it. Each model below was built against the pinned compiler and checked on functions that had already matched before it was used on new ones:
| Model | What it explains | Check |
|---|---|---|
| Register allocator | How named values are coloured, and how expression temporaries rotate through EAX, ECX and EDX | 99.86% of held-out cases |
| Spill slots | How stack slots are sorted by size and reference count, and packed into dead parameter homes | About 470 matched frames, no mismatches |
| Scheduler | How each block is reordered for the Pentium’s two pipes | 5,765 of 5,765 held-out blocks |
| Alias rules | Which loads and stores the scheduler may swap, including a 31-location limit per base pointer | Five new matches in three waves |
| Block copies | Why writing a shared block once per case shifts the registers after it | Two switch functions stuck since early on |
The recipes stopped being devices that happened to work and became explanations of why they work. The models also sorted the remaining functions by what was actually wrong: of 83 examined at one point, 9 differed only in register colour, 6 only in scheduling, 68 in structure, and none only in stack slots.
Making it readable
For most of the project, credited sources were frozen, which left them full of names like sub_0043BFE0. Decision 0027 allows a credited source to take a readability change (names, types, struct members, comments) only if recompiling every affected function gives exactly the credited bytes. A change that alters a single byte is rejected whole, not tuned.
The names come from a knowledge base in which every entry carries its evidence and a confidence level. Fourteen research passes went through the engine one subsystem at a time: the script interpreter, with 144 opcodes named; actor structures, with 179 fields; rooms and file formats; rendering; audio; menus and saves; the platform layer, where 193 small wrappers recreate the console SDK the game was ported from; collision, effects, text and maths. Then a stricter review downgraded 267 of the 281 entries marked “confirmed”, keeping only the 14 backed by a string, an import or SDK identity. A spot check of 400 names judged 85% right, 1% wrong and 13% weak, and the wrong and weak ones were fixed.
Writing up the architecture exposed contradictions between researchers, which were settled against the binary. A function one researcher had called a script restart turned out to run on every tick, so it is now the per-tick script step.
Applying the names hit one wall. Adding any declaration above a function shifts the compiler’s internal label numbers, and the reviewed switch envelopes pinned those labels, so hundreds of renames were being refused even though every byte was identical. A tool now updates only those label names, and only after proving every byte, relocation, table and label offset unchanged. Since then, more than a thousand credited sources read with proper function, field and parameter names.
The final hundred
From wave 148 the matching sped up again, with the models in hand and the recipe library sorted into a playbook by kind of mismatch. Waves 148 to 164 took the game-code measure from 93.1% to 98.6%. Along the way:
- Floating-point constants. When source says
x * 8.0f, the compiler stores 8.0 as a tiny named constant that the linker pools. Every attempt to write it another way changed the instruction order, so a literal was the only faithful source. Decision 0033 admits exactly those constants, checked bit for bit against the original’s read-only data, with no credit for their bytes. - A function that was not a function. A six-byte “function” at
0x004700B0turned out to be the tail of the one before it, which has no return of its own and runs straight into it. Its only evidence had been a number in a data table that happened to look like an address. It was retired and its predecessor registered at its real length. - One instruction. A sound decoder at
0x00411270was parked for days one instruction away: the compiler emittedxor eax,7where the original hascmp eax,7. It matched on its twentieth attempt, after a worker found that reading a value twice in a condition stops the compiler substituting it away. - The patched function. The last function Classic REbirth rewrites internally, and so the last one the source build still had to borrow, matched in wave 153.
By wave 165 a census of the game’s code left exactly 14 functions, 13,239 bytes, every one of them attempted many times before. There was no unexplained remainder and no untried root.
One old theory was settled here, and it corrects an earlier version of this page. The near misses had looked like the work of a different compiler build, since the metadata names builds I had not tested. I sourced the Service Pack 3 compiler too and compiled all 134 logged attempts for the twelve functions then remaining under it. Every one was byte-identical under all three builds. The compiler does not explain the gaps; they are in the source, which means they can be closed by writing better source.
Six of the fourteen have closed since then. The last, at wave 174, was a 1,534-byte switch function. A trace of the compiler’s internal sort keys showed that a value only merged into a shared s->z0 / 18 the way the original’s did if that division was evaluated first, so the fix was to name the earlier quotient before the test that uses it, and five older devices could be deleted.
Where it stands
As of wave 174, on 22 October 2026, the pipeline has recovered 3,687 functions, verified against the original compiler’s output. Of the game’s own code, 99.2% is recovered source; eight functions, 7,798 bytes, remain. The closest has the original’s size and stack frame and differs only by two registers swapped in its last few instructions. Another has the right size and four differing lines.
None of this means the decompilation is finished. Eight functions are left, and some have resisted dozens of attempts. Matching every function is not the end either: the final milestone is a clean build whose whole file is identical to the original executable, with every byte accounted for.
What the project does already show is narrower and, to me, more interesting. Precise work can be split across many workers without anyone trusting anyone, as long as there is a judge that cannot be argued with, every claim is checked against it, and the records keep the failures as carefully as the wins.
The recovered source also runs as a modern Windows port, with high frame rates and widescreen and the same game logic, tick for tick. That is its own story: Porting Resident Evil 3 to modern PCs.
The work uses my own copy of the game. No game assets or code are distributed.