Repository navigation
Vector Units
VU0 and VU1 are vector processors with paired instructions, delayed results and separate pipeline resources. Correct VU execution includes when a result becomes visible, not only its final numerical value.
DQ8Recomp keeps an interpreter and adds ahead-of-time compiled VU code. The generator reads locally supplied code images or execution profiles and emits C++ specializations. The host compiler builds these with the runtime; no executable memory is generated during play. Game microcode and captured profiles are not distributed with the project.
The registry matches the VU unit and every instruction byte before selecting a native block or individual pair. Unknown or unsupported code falls back to the interpreter. Code uploads invalidate affected decoded/native cache entries.
Eligible ordinary regions retain VF lanes, VI values, ACC and immediate-I versions in local variables. A compile-time plan calculates hazards and result visibility, avoiding repeated instruction dispatch and pipeline bookkeeping. The region publishes the visible state and its still-pending pipeline tail on exit.
The accepted implementation supports ordinary regions up to 16 instruction pairs and selected straight conditional loops up to 32 pairs. Retained loops can carry delayed CLIP and visible/pending Q across iterations. Eligibility remains deliberately narrower than the full instruction set. Larger ordinary regions and general control-flow graphs are not part of the accepted implementation.
Guarded ARM64 arithmetic accelerates common inputs. Exceptional values and rounding boundaries keep exact fallback calculations. Unsupported runtime conditions also fall back; static compilation is not permission to approximate PS2 floating-point behavior.
VU1 microprograms can also be compiled whole, from the MSCAL entry to the E bit. The regions above still leave the interpreter to run everything between them a pair at a time, and in DQ8's field that per-pair work was most of what VU1 cost; compiling whole programs took the opening field from about 15 to about 22 FPS, and renderer work did the rest of the way to 30.
A routine is the code reachable from one entry through branches. Calls and register jumps end it, and their targets become routines of their own; a jump through a register loaded from VI0 just before is resolved at compile time. Pairs still issue in order under the interpreter's stall rules, but each pair's hazards are decided when the routine is compiled, results are written at issue with only their ready cycles kept, and MAC, status and clip results wait in a small queue until something reads them. The clock and that queue stay in registers. Whatever is still in flight when a routine returns goes back into the interpreter's pipelines, so a later pair or a flush sees what it would have seen.
At run time a routine is looked up by PC and checked word for word against VU memory, so changed microcode falls back to the interpreter, as does a pair the compiler does not handle. A routine only starts with at least 4096 cycles of budget and hands back at a block boundary when the budget would run out.
The common FMAC cases use NEON on AArch64 and SSE2 on x86, with range checks that send anything unusual to the interpreter's arithmetic. Other hosts compile a plain C++ path. All three can be tested on an ARM machine: PS2X_VU_PROGRAM_SSE builds the SSE2 path through sse2neon, and PS2X_VU_PROGRAM_PORTABLE the plain one. On the field captures the routines run about 9 times faster than the interpreter with NEON, 8 to 9 times with SSE2 run through sse2neon, and 5 times on the plain path. CI runs the SSE2 path on an x86 CPU, but it has not been timed on one yet.
Changes must preserve:
- VF/VI/ACC write latency, partial destination masks and cancellation of older pending writes.
- Immediate-I ordering within an instruction pair and CLIP/MAC/STATUS visibility, including sticky flags.
- Branch delay slots and the previous VI value required by branch forwarding rules.
- Visible versus pending Q, DIV resource stalls and D/I flags; reading Q does not imply waiting for it.
- Arbitrary cycle-budget exits, stop requests, code invalidation and resumption.
- Store ordering and GIF packet contents/timing around XGKICK and callbacks.
- Constant registers, floating-point normalization, rounding and exceptional arithmetic.
An incoming pending result may still be invisible when a compiled region begins. Likewise, a result from the last few instructions may need to remain pending after exit. Publishing either early can pass a whole-program comparison while failing a short resumed execution.
After reaching a scene in an interpreter build, an execution profile can record previously uncompiled four-pair entries:
mkdir -p build/vu-local
PS2_VU_AOT_PROFILE="$PWD/build/vu-local/field.vublocks" \
./build/game/src/runtime/dq8 ../Extracted_Usa/SLUS_212.07 \
--iso=../DQ8_USA.iso --gs=sdlgpu
cmake -S . -B build/game \
"-DPS2X_VU_AOT_IMAGES=$PWD/build/vu-local/field.vublocks"
cmake --build build/game --target dq8 dq8-vu-replay --parallel 2Use a new profile filename for each recording and remove the recording variable before timing. Profiles cover only executed code; reaching native coverage does not eliminate all per-instruction work.
For differential inputs, set PS2_VU_CAPTURE_DIR to a fresh local directory.
PS2_VU_CAPTURE_TRIGGER can name a file created after reaching the desired scene.
Captures contain .code, .data, and .state files and require the current state
layout. The recorder only takes states with drained pipelines; it does not replace
authored tests of in-flight state.
The replay driver's --emit-aot=path.vublocks emits executed block entries from a
capture. Raw 4 KiB VU0 and 16 KiB VU1 code images are also accepted by the generator.
PS2X_VU_AOT_IMAGES is a semicolon-separated list of absolute input paths;
PS2X_VU_AOT_PAIR_IMAGES adds individual pairs, and PS2X_VU_AOT_LOOP_IMAGES scans
local code images for supported retained loops. Wider-profile selection and
coverage analysis are developer workflows, not an automatic game installer.
All three lists must be empty to disable generated VU code.
Generated code defaults to 32 shards with two concurrent runtime compilation jobs
under Ninja. PS2X_VU_AOT_SHARDS and PS2X_VU_AOT_COMPILE_JOBS control these limits.
Keep inputs, generated registries and capture output local and out of releases.
Whole programs are recorded the same way, into a directory rather than a file:
PS2_VU_PROGRAM_PROFILE="$PWD/build/vu-programs/field" \
./build/game/src/runtime/dq8 ../Extracted_Usa/SLUS_212.07 \
--iso=../DQ8_USA.iso --gs=sdlgpu
cmake -S . -B build/game \
"-DPS2X_VU_PROGRAM_PROFILES=$PWD/build/vu-programs/field"
cmake --build build/game --target dq8 --parallel 2Each entry reached without a compiled routine is saved as a 16 KiB code image and
a line in entries.txt, so recording again adds to the same directory. Play
through the scenes you want covered; routines only exist for code that ran. The
list may name several directories. The build turns them into
PS2X_VU_PROGRAM_SHARDS compilation units (8 by default), which for DQ8's
opening and field take under ten seconds to compile.
Recordings cannot be shipped, but where their code sits in the ELF can.
config/SLUS_212.07/vu1-programs.txt lists, for each recorded image, the runs
of pairs its routines read, as offsets into SLUS_212.07 with one checksum per
image. setup.py recompile rebuilds the images from the player's ELF with
vu_program_manifest.py expand, and the build compiles them like a recording.
To extend the list, record the new scenes and make it again from the current
list's expansion plus the new recordings. The expansion stands in for the
recordings it came from, so nothing already covered is lost:
python3 thirdparty/PS2Recomp/tools/vu_program_manifest.py make --uploads \
--output config/SLUS_212.07/vu1-programs.txt \
--game ../Extracted_Usa/SLUS_212.07 \
build/generated/SLUS_212.07-vu1 build/vu-programs/town--uploads also describes the microcode the ELF uploads with VIF MPG that no
recording reached, as if it had been loaded in place of a recorded program at
the same address and entered at the same points.
The principal files are under thirdparty/PS2Recomp/ps2xRuntime/src/lib/vu/:
| Files | Purpose |
|---|---|
ps2_vu1_core.cpp, ps2_vu1_cache.cpp
|
Execution, decode, pipeline retirement and code identity |
ps2_vu1_upper*, ps2_vu1_lower*
|
Arithmetic and lower-instruction semantics |
ps2_vu1_region* |
Static scheduling, local values and region publication |
ps2_vu1_loop* |
Retained-loop planning and execution |
ps2_vu1_exec.inl |
Compiled execution selection and fallback |
ps2_vu1_program.* |
Whole-program routines: per-pair issue, flag queue and hand-back |
The generators are thirdparty/PS2Recomp/tools/compile_vu_blocks.py and compile_vu_programs.py. Authored tests live in the submodule's ps2xTest/; the parent project's replay driver is tools/runtime/vu_replay.cpp. Whole programs are tested on synthetic programs from ps2xTest/data/vu_programs.py, compiled and interpreted side by side; see Testing.
Start with a small authored program that proves the intended optimization is actually entered. Test partial budgets and subsequent raw execution, not just final results. Include a negative control that deliberately breaks the semantic rule and must fail. Then run captured differential replays and measure accepted coverage on the real workload.
PS2_VU_EXECUTION=interpreter selects the interpreter for diagnosis. The replay verifier additionally uses an independent raw-byte reference path. Agreement with that reference protects against regressions; it is not by itself proof of agreement with original hardware.
Exact byte matching and fallback preserve a path for modified microprograms: changed code cannot silently reuse an incompatible specialization. A faster modified program may require rebuilding its local AOT profile. This is infrastructure for future modding, not a released modding interface.
See Testing for replay commands and measurement requirements.
Users
Developers