Translates PlayStation 3 RSX (NV47xx) GPU shader microcode into HLSL, which DXC or glslang can then compile to DXIL and SPIR-V for D3D12 and Vulkan.
This is the PS3 counterpart to XenosRecomp for the Xbox 360: an offline batch recompiler that turns a game's shaders into a hash-indexed cache, so nothing has to be translated at runtime.
$ rsxrecomp decode shader.vp.bin
0: DP4 o[0]:HPOS.x, v[0], c[0]
1: DP4 o[0]:HPOS.y, v[0], c[1]
2: DP4 o[0]:HPOS.z, v[0], c[2]
3: DP4 o[0]:HPOS.w, v[0], c[3] // END
$ rsxrecomp decompile shader.vp.bin -o shader.hlsl
Both the vertex and fragment pipelines run end to end: microcode → disassembly → IR → HLSL → SPIR-V.
- 94 of 96 opcodes reach compilable HLSL (
docs/COVERAGE.md). The two that do not are the vertexPSH/POPstack operations, whose semantics are undocumented in every source consulted. - Validated on a shipped game: every shader God of War III uploaded to the RSX during a play session — 39 vertex programs numerically equivalent, 74 fragment programs decoded byte-exact and compiled (see below).
- 693 tests, including compiling all 51 fixtures to SPIR-V with a real HLSL front-end.
- Validated against an independent implementation. 20 shaders compiled
by
cgcomp, the PS3 homebrew scene's own shader compiler, decode to exactly the assembly they were built from. - Verified numerically. The generated HLSL is compiled and executed, and its output compared against a reference interpreter on thousands of random inputs — so the claim is that the translation computes the same values, not merely that it compiles. See "What is proven" below.
git clone <this repo> && cd rsxrecomp
pip install -e .Only xxhash is required. pytest and glslang are for the test suite.
rsxrecomp decode shader.bin # disassembly
rsxrecomp ir shader.bin # lowered intermediate form
rsxrecomp decompile shader.bin -o out.hlsl # HLSL
rsxrecomp scan EBOOT.BIN # find shaders in a binary
rsxrecomp hash shader.bin # XXH3-64 cache keyUseful flags:
| Flag | Why |
|---|---|
--stage {vertex,fragment,auto} |
Which decoder to use. auto reads the filename, then guesses structurally |
--endian {be,le,auto} |
PS3 microcode is big-endian; emulator caches are usually little |
--offset 0x1234 |
Decode a program embedded partway into a file |
--depth-output |
Fragment: declare that the shader writes depth via R1.z |
--render-targets N |
Fragment: how many colour buffers are bound (R2..R4 are temporaries otherwise) |
--color-precision fp16 |
Fragment: colours come from H0/H4/H6/H8 rather than R0/R2–R4 (default auto: a container's control word, else inferred) |
--texture-dim 0=cube |
Declare a unit's dimensionality (1d/2d/3d/cube/2d_array) |
--all-inputs |
Declare all 16 vertex attributes, not just the ones read |
--constant-count N |
Size of the emitted constant bank (default 468) |
Batch-extract everything in a game binary:
python3 tools/extract_shaders.py EBOOT.BIN -o extracted/This writes each shader's raw bytes, disassembly and HLSL, plus a
manifest.json keyed by cache hash and a report.txt listing anything the
decoder was unsure about.
scanner.py -> decoder/{vp,fp}_decoder.py -> ir/builder.py -> emit/hlsl.py
find microcode to instructions IR to
shaders instruction records to IR HLSL
The IR in the middle earns its place two ways: two decoders feed one backend,
and being able to dump the IR (rsxrecomp ir) makes it possible to attribute
a wrong result to the decoder, the lowering, or the emitter.
Design decisions and their reasoning are in docs/ARCHITECTURE.md.
Vertex programs become VSOutput main(VSInput input) with ATTR0..15 inputs,
a float4 c[468] constant buffer, and NV40 output registers mapped to their
D3D semantics. Fragment programs become PSOutput main(PSInput input) with
matching interpolator semantics, so a VS/PS pair links.
Branching vertex programs become a dispatch loop over a switch on a program
counter, with an explicit call stack for CAL/RET — HLSL has no goto.
Straight-line programs skip that entirely so the compiler can optimise them.
Fragment control flow encodes nested regions with explicit end offsets, so it
reconstructs directly into structured if and for.
| File | Contents |
|---|---|
docs/NV40_VP_ISA.md |
Vertex program encoding, field by field, with sources and known unknowns |
docs/NV40_FP_ISA.md |
Fragment program encoding, including the RSX 16-bit half-swap |
docs/ARCHITECTURE.md |
Design decisions, what was taken from where, limitations |
docs/COVERAGE.md |
Per-opcode support, generated from the code |
Most of the fixtures were assembled by this project's own encoder, which proves self-consistency but not correctness — a mistake shared between the encoder and the decoder round-trips cleanly.
tests/fixtures/real/ closes most of that gap. Those are genuine RSX
microcode emitted by cgcomp, the PS3
homebrew scene's shader compiler: a separate implementation, in C++, by
different people, from independent reverse engineering. Twenty shaders decode
to exactly the assembly they were compiled from.
Confirmed against that independent implementation: instruction encoding
and word order for both stages, the fragment 16-bit half-swap, opcode
numbering, register-file selectors, the attribute and output register maps,
swizzles, writemasks, negation, the saturate flag, and the NV40 quirk that the
vector ADD reads src0 and src2.
Two shaders decode differently from their source, and in both cases the raw
bits show the reference compiler is wrong: D1 holds a single const_src
field, so one instruction physically cannot read two constants, and cgcomp
silently dropped one without reporting an error.
Reading the instructions correctly is not the same as computing the same
answer. rsxrecomp/verify.py runs two independent implementations on
identical random inputs and compares the results:
rsxrecomp/interpret.pyexecutes the decoded microcode directly, written from the ISA semantics rather than reusing the compiler path;- the generated HLSL goes through glslang to SPIR-V, through spirv-cross to C++, and is compiled and run.
Nothing in the second chain is ours once the HLSL leaves the emitter.
python3 tools/verify_numerically.py 'tests/fixtures/real/*.vpo' --trials 2048Every vertex shader in the project agrees to fp32 precision — including branches, subroutine calls, relative constant addressing, and condition-code predication. The suite carries a negative control that corrupts a shader and asserts the comparison catches it, because a harness that can only pass proves nothing.
Where fp32 genuinely cannot represent a result — a dot product that cancels by
~8000× before a reciprocal — the harness re-runs in double precision and
reports the case as ill-conditioned rather than loosening the tolerance until
it disappears. See docs/ARCHITECTURE.md.
God of War III (BCES00510) was run in RPCS3, which keeps a raw copy of every
program a game uploads to the RSX. tools/validate_rpcs3_cache.py runs the
whole pipeline over such a cache:
python3 tools/validate_rpcs3_cache.py ~/Library/Caches/rpcs3/cache/BCES00510| Stage | Programs | Instructions | Result |
|---|---|---|---|
| Vertex | 39 | 1,147 | 39 numerically equivalent (worst error 1.05e-05, none ill-conditioned) |
| Fragment | 74 | 4,206 | 74 decoded with every byte, inline constants included, consumed; 74 compiled to SPIR-V |
The game exposed three defects that neither the synthetic nor the cgcomp fixtures could:
- An undeclared variable. A vertex instruction writing a temporary and an output at once (13 of 39 programs) produced HLSL that did not compile. The test for that shape checked the text and never compiled it; it now runs numerically.
- Five missing fragment opcodes, 0x38–0x3E (
DP2,NRM,DIVSQ,FENCT,FENCB), used by 49 of 74 programs. Evidence indocs/NV40_FP_ISA.md. - The wrong colour register. 73 of 74 programs export fp16 colour through
H0; the emitter hard-codedR0. Colour now follows the shader-control word's 32-bit-exports bit.
Game shaders are copyrighted and are not in this repository; captures stay in
the gitignored private/ directory.
Still not proven:
- Fragment programs numerically. The harness drives the vertex stage; fragment verification also needs a sampler model. The game's fragment shaders are shown to decode and compile, not to render correctly.
- Colour-register inference on raw captures. A capture carries no control
word, and 62 of the 74 programs write both
R0andH0; the decompiler takes the last written. Every piece of evidence agrees, but it is inferred. - DXC. Output is compiled with glslang; DXC, which a D3D12 backend would use, has not been run.
- More than one game, and more than one play session of this one.
ps3recomp already has in-tree C decompilers for RSX vertex and fragment programs, wired into its D3D12 backend. This project is not filling a hole that leaves; it is a standalone, tested, documented recompiler that runs offline over a whole game's shader set and emits HLSL with a manifest keyed by the XXH3-64 cache hash — the XenosRecomp shape rather than the runtime-decompiler shape. Compiling that HLSL and packing it into a runtime cache is still left to the caller.
MIT. Clean-room from MIT sources and public specifications:
- Mesa nouveau
nv40_vertprog.h/nvfx_shader.h(MIT) — bit layouts and opcode tables - envytools
rnndb(MIT) — NV4097 methods and register file sizes NV_vertex_program2/3,ARB_vertex_program,ARB_fragment_program— operation semantics- ps3recomp (MIT) — the RSX fragment half-swap and RSX-specific field widths
RPCS3 is GPL and was used only as a read-only oracle for field widths and opcode values. No RPCS3 code was copied, adapted, or translated.
pytest tests/ # 693 tests
python3 tools/make_fixtures.py # regenerate vertex fixtures
python3 tools/make_fp_fixtures.py # regenerate fragment fixtures
python3 tools/coverage_report.py # regenerate docs/COVERAGE.mdTests that need an HLSL front-end skip rather than pass when none is
installed, so a green run without one is visibly weaker. Install glslang
(brew install glslang) to enable them.