ITADN
celsowm/vf2-decomp
README.md

vf2-decomp

Clean-room, non-matching C17 decompilation research project for Virtua Fighter 2 Version 2.1 on Sega Model 2A.

This repository does not contain ROMs and is not yet a playable port. It contains ROM validation and reconstruction tools, a structured Intel i960 analyzer, a bounded semantic executor used for differential validation, and the first game/runtime functions recovered in portable C.

v0.1.3 fifth-dispatch acceptance

The recovered native runtime now crosses the complete observed fourth scheduler sweep and the next frame boundary. vf2i960 native-fifth-dispatch reaches the fifth fa_game_info entry with:

  • 830 repeated-frame differential blocks;
  • 7,402,741 instructions on both native and reference sides;
  • 8,673,563 continuous recovered instructions including the historical bridge;
  • complete CPU, local-frame, execution-counter, frame-event and mutable-memory equality at every checkpoint; and
  • zero interpreted instructions on the native side.

The extension recovers the observed non-zero texture stream: a 35,059- instruction five-level mip expansion consuming 43,648 source bytes and writing 87,296 texture bytes. It also accepts leading inactive texture records before a live record, the zero-counter texture path and the distinct mode-17 diagnostic instruction profile.

v0.1.2 fourth-dispatch acceptance

The recovered native runtime now crosses the complete observed third scheduler sweep and another frame boundary. vf2i960 native-fourth-dispatch reaches the fourth fa_game_info entry with:

  • 78 repeated-frame differential blocks;
  • 58,869 instructions on both native and reference sides;
  • 1,329,691 continuous recovered instructions including the original bridge;
  • complete CPU, local-frame, execution-counter, frame-event and mutable-memory equality; and
  • zero interpreted instructions on the native side.

The extension recovers the observed player-update bit-14 fast exit, active game input/state selector paths, frame modes 16/17 in the memory diagnostic, the active frame-counter selector exit and the observed phase-17 dispatcher path. Rejected player/video variants now preserve caller CPU state and avoid partial video-memory writes for the covered rejection points.

v0.1.1 repeated-corridor hardening

After the v0.1.0 repeated-frame acceptance, the runtime was hardened for the first dynamic states exposed by continued execution:

  • scheduler completion when the final active task changes late in a sweep;
  • both observed recurring fa_kill_osage order-bit instruction profiles;
  • expiration of the observed texture counter and creation of its pending upload;
  • the observed seven-row, three-plane palette translation/upload path; and
  • aligned CMake, public header, executable and documentation versioning.

Unsupported texture variants and unobserved state combinations still fail with VF2_ERROR_UNSUPPORTED; the native runtime never silently falls back to the i960 interpreter.

v0.1.0 repeated-frame acceptance

The supported Version 2.1 ROM set validates a continuous recovered path from the completed first scheduler sweep through the third scheduler entry. vf2i960 native-third-dispatch reaches the third fa_game_info task with:

  • 42 repeated-frame differential blocks;
  • 55,239 instructions on both the native and reference sides;
  • 1,326,061 continuous recovered instructions including the original post-frame bridge;
  • complete CPU, local-frame, execution-counter and mutable-memory equality; and
  • zero interpreted instructions on the native side.

The repeated corridor includes another frame interrupt/return, persistent task contexts, texture/video/game/tile repeated paths and the large phase-16 frame dispatch handler. This acceptance does not make the project playable: TGP rendering, broader gameplay states, audio, input and a production platform backend remain on the roadmap.

v0.0.25 milestone

v0.0.25 completed the consolidation work that prepared this acceptance:

  • consolidated project status and roadmap documents;
  • mapped known unobserved/uncovered scheduler and task branches in docs/UNCOVERED_BRANCHES.md;
  • split the recovered post-frame bridge into modular texture, video, geometry, input and match subsystems; and
  • added multi-frame runtime tests for frame waiting and external interrupts.

v0.0.24 milestone

The accepted startup path from the end of the first scheduler sweep through the second entry into fa_game_info executes entirely as recovered C. The reference i960 interpreter is still advanced in the differential validator, but it is no longer used to produce any CPU or Model 2 state on the native side.

The strict ROM-backed bridge totals are:

  • 1,270,822 total original bridge instructions;
  • 1,270,822 instructions reproduced by recovered C;
  • zero interpreted instructions on the native path;
  • 190 semantically composed blocks, each followed by a complete CPU and mutable-memory checkpoint;
  • 342 recovered procedure calls and 340 recovered returns;
  • the four-visit frame wait, vector-12 interrupt injection and interrupt return reproduced by the recovered frame scheduler;
  • complete CPU, local-frame and all mutable-memory regions matching at the second fa_game_info entry.

The lower block count is intentional: small helpers were absorbed into complete call-site, interrupt and main-loop procedures while retaining one implementation of each underlying semantic helper.

This milestone does not mean that Virtua Fighter 2 is fully decompiled or playable. It proves the single observed Version 2.1 startup/second-dispatch path. Unobserved branches continue to return VF2_ERROR_UNSUPPORTED, and substantial TGP, rendering, gameplay and audio work remains.

Build

cmake -S . -B build \
  -DVF2_BUILD_TESTS=ON \
  -DVF2_WARNINGS_AS_ERRORS=ON
cmake --build build
ctest --test-dir build --output-on-failure

To enable the ROM-backed differential tests, point CMake at a legally obtained supported ROM directory:

cmake -S . -B build \
  -DVF2_BUILD_TESTS=ON \
  -DVF2_WARNINGS_AS_ERRORS=ON \
  -DVF2_ROM_DIR=/path/to/vf2
cmake --build build
ctest --test-dir build --output-on-failure

Sanitizer build:

cmake -S . -B build-san \
  -DVF2_BUILD_TESTS=ON \
  -DVF2_WARNINGS_AS_ERRORS=ON \
  -DVF2_ENABLE_SANITIZERS=ON \
  -DVF2_ROM_DIR=/path/to/vf2
cmake --build build-san
ctest --test-dir build-san --output-on-failure

Main tools

Validate and reconstruct the supported ROM set:

build/vf2rom verify /path/to/vf2
build/vf2rom info /path/to/vf2
build/vf2rom extract /path/to/vf2 out/regions

Analyze the i960 program:

build/vf2i960 disasm /path/to/vf2 0x0001d320 64
build/vf2i960 function /path/to/vf2 0x0001d320
build/vf2i960 analyze /path/to/vf2 out/analysis

Exercise the recovered runtime milestones:

build/vf2i960 compare-boot /path/to/vf2
build/vf2i960 compare-init /path/to/vf2
build/vf2i960 compare-task-registry /path/to/vf2
build/vf2i960 compare-timer-irq /path/to/vf2
build/vf2i960 scheduler-dispatch /path/to/vf2
build/vf2i960 compare-first-dispatch /path/to/vf2
build/vf2i960 compare-camera-classifier /path/to/vf2
build/vf2i960 compare-camera-viewport /path/to/vf2
build/vf2i960 hybrid-first-dispatch /path/to/vf2
build/vf2i960 native-first-dispatch /path/to/vf2
build/vf2i960 native-second-dispatch /path/to/vf2
build/vf2i960 native-third-dispatch /path/to/vf2
build/vf2i960 native-fourth-dispatch /path/to/vf2
build/vf2i960 native-fifth-dispatch /path/to/vf2
build/vf2i960 compare-texture-bridge /path/to/vf2
build/vf2i960 compare-post-frame-bridge /path/to/vf2
build/vf2i960 compare-geometry-boundary /path/to/vf2
build/vf2i960 compare-second-scheduler-entry /path/to/vf2
build/vf2i960 compare-game-geometry-helpers /path/to/vf2
build/vf2i960 task-profile /path/to/vf2 out/first-dispatch.csv

First-dispatch profile

fa_game_info      19 instructions, 0 calls
fa_camera       2700 instructions, 6 calls
fa_user            1 instruction,  0 calls
fa_sound          15 instructions, 0 calls
fa_kill_osage     36 instructions, 2 calls
fa_osage0         19 instructions, 0 calls
fa_osage1         18 instructions, 0 calls

The task paths, scheduler transitions and camera recovery boundaries are documented in docs/FIRST_DISPATCH_TASKS.md, docs/HYBRID_EXECUTION.md, decomp/i960/notes/camera_initialization.md, decomp/i960/notes/camera_recurring_update.md, decomp/i960/notes/camera_viewport.md, decomp/i960/notes/post_frame_texture_bridge.md, decomp/i960/notes/geometry_bridge_v0020.md and decomp/i960/notes/second_scheduler_entry_v0021.md.

Repository layout

config/                 exact supported ROM manifest
decomp/i960/            symbols, task descriptors and evidence notes
docs/                   architecture, status, execution and roadmap
include/vf2/            public C APIs
src/analysis/           CFG, xrefs, semantics and pseudocode generation
src/i960/               decoder, executor and snapshots
src/hardware/           bounded Model 2A memory/device model
src/recovered/          accepted semantic C recoveries
tools/vf2rom/           ROM validation and region reconstruction
tools/vf2i960/          analysis and differential-validation CLI
tests/                   ROM-independent and optional ROM-backed tests

The repository contains no Sega game data. Users must provide their own legally obtained ROM files. See docs/LEGAL.md and THIRD_PARTY.md.

Snapshot endurance runner

The strict fifth-dispatch command can optionally persist its proven native boundary as a versioned snapshot:

build/vf2i960 native-fifth-dispatch /path/to/vf2 fifth-dispatch.vf2snap

vf2cycles restores that exact CPU and mutable Model 2 state into independent reference and recovered-native machines, then executes repeated scheduler cycles in differential lockstep:

build/vf2cycles \
  --rom-dir /path/to/vf2 \
  --snapshot fifth-dispatch.vf2snap \
  --cycles 10 \
  --min-blocks 1 \
  --max-blocks 16384

The fifth-dispatch command writes both fifth-dispatch.vf2snap and the versioned fifth-dispatch.vf2snap.runtime host-state sidecar. vf2cycles loads the sidecar automatically, or accepts an explicit --state path.

The command stops at the first unsupported native block, reference execution failure or state mismatch and prints the partial cycle, last recovered step and first differing component. Add --failure-prefix fifth-sweep-failure to write the last fully matched pre-block state as .vf2snap, .runtime and .txt files. Re-running those files reproduces the unsupported block without replaying the accepted corridor. Pre-block snapshots are only taken when --failure-prefix is present, so normal endurance runs avoid that extra copy. A successful run never interprets instructions on the native side.

A ROM-backed strict endurance chain from the fifth-dispatch checkpoint has now completed 10,000 additional repeated-address cycles (360,000 blocks / 15,689,445 recovered instructions) with exact per-block differential state equality. The final chained checkpoint is again fa_game_info at 0x0001645c, with scheduler entry / frame IRQ 10,003. The strict comparator now checks the two live CPU and mutable-memory states directly, snapshot capture reuses same-sized region buffers, and equal regions take a memcmp fast path while mismatches retain byte-precise diagnostics.

For longer scouting runs, --boundary-probe keeps the same recovered-native and reference instruction-count lockstep but defers the expensive complete snapshot comparison until the repeated address closes each cycle. If a probe fails, both machines and the runtime sidecar are restored to the exact start of that cycle, so the same checkpoint can be replayed immediately with the strict per-block runner. Successful probe cycles prove cycle-boundary equality only; they do not replace the strict acceptance contract.

build/vf2cycles \
  --rom-dir /path/to/vf2 \
  --snapshot fifth-dispatch.vf2snap \
  --cycles 1000 \
  --boundary-probe \
  --failure-prefix boundary-failure \
  --output-snapshot next-boundary.vf2snap

--output-snapshot writes both the requested .vf2snap and its .runtime sidecar after a successful run, allowing long endurance work to resume without replaying earlier accepted cycles. Using the verified ROM set, boundary probes have reached scheduler entry / frame IRQ 16,384 with complete cycle-end CPU, counter, local-frame and mutable-memory equality. The stricter published per-block corridor is now the 10,000-cycle result above.

Controlled state-transition probing has also recovered both phase-17 navigation directions. Gameplay bit 13 (0x00002000) steps backward (11 -> 10, with 0 -> 11 wrap), while mask 0x08001008 steps forward (10 -> 11, with 11 -> 0 wrap). Both update the old/new double-indirect phase target markers to 0x8020/0x801c; strict differential replay matches the enclosing 281/282- instruction main-final-cluster variants and their complete following 36-block cycles. This is targeted branch evidence rather than an extension of the passive endurance claim.

The next controlled phase transition is now recovered as well. Injecting 0x04000104 at the same pre-main-final-cluster boundary sets phase-index bit 7, clears the phase auxiliary byte, stores 0xff in the companion state byte, zeros the current object marker, clears the 48x64 tile plane to word 0x0020, and centers the ROM phase label (EXIT for index 11) through the original 0x00060410 -> 0x00007fc0 text path. Strict differential replay matches the 12,889-instruction enclosing cluster exactly and stays equal for the following 35 blocks.

That bit-7 continuation is now recovered for the observed index-11 entry. 0x00059154 clears the bit and dispatches through 0x0005ff00 to 0x0005ef60. On the first visit the recovered path reproduces the game-meter update, 15-byte CRC, 48x64 tile clear and EXIT TEST MODE draw; the frame dispatcher accounts 13,286 instructions and the enclosing main-final-cluster matches strictly at 13,518 instructions. The path then arms a 320-frame countdown. Positive countdown visits strictly match at 626 dispatcher / 858 cluster instructions. A cycle-boundary probe from counter 320 validates 319 cycles (11,484 blocks / 701,481 instructions) and restores an exact checkpoint at counter 1. Strict replay now continues through the terminal counter 1 -> 0 transition at 0x0005f07c: the recovered path clears the observed video/gameplay state, clears the 48x64 tile plane, emits the ROM RESET diagnostic through 0x0006116c, and performs the non-returning branch to 0x000000b0. The enclosing main-final-cluster matches at 13,426 instructions.

The native runtime also accepts that warm soft-reset handoff through the existing boot recovery without pretending it is a power-on reset. Boot stage 1 preserves untouched incoming registers/control state and matches 1,180,053 instructions to 0x000001b0; boot stage 2 matches another 182,514 instructions to 0x0000052c. Together with the terminal cluster, the three strict blocks cover 1,375,993 instructions with exact CPU, local-frame, procedure-counter and mutable-memory equality. The warm-reset continuation now extends through the observed post-boot initializer to caller boundary 0x000098b0. The existing 0x0000052c -> 0x0006dd4c prefix contributes 60,078 strict instructions; from 0x0006dd4c, 15 additional recovered blocks contribute 1,498,968 instructions with exact CPU, local-frame, procedure-counter and mutable-memory equality after every block. Combined with the terminal and warm boot stages, the controlled soft-reset chain is now compositionally proven for 2,935,039 instructions.

The recovered initializer includes both descriptor-driven bulk streams (4 descriptors / 464 words and 22 descriptors / 92,672 halfwords), valid backup-SRAM probe/CRC/restore, both video-ramp passes, palette/table construction and the observed geometry/video hardware-core setup. The second ramp is intentionally 11,245 instructions rather than the first pass's 11,563 because its restored 0x40/0x25 controls take different clamp branches; runtime accounting derives that count from the data rather than the call site. The next concrete recovery boundary is the call at 0x000098b0 into the texture/graphics initializer 0x0004b020. Other bit-7 table entries and phase state zero remain unsupported.