Skip to content
TalhaMudoPublic

About

PS3 RSX (NV47) vertex/fragment shader microcode to HLSL recompiler

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

12 Commits

Folders and files

Repository files navigation

RSXRecomp

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

Status

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 vertex PSH/POP stack 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.

Install

git clone <this repo> && cd rsxrecomp
pip install -e .

Only xxhash is required. pytest and glslang are for the test suite.

Usage

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 key

Useful 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.

How it fits together

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.

Generated output

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.

Documentation

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

What is proven, and what is not

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.

Numerical equivalence

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.py executes 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 2048

Every 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.

A shipped game

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 in docs/NV40_FP_ISA.md.
  • The wrong colour register. 73 of 74 programs export fp16 colour through H0; the emitter hard-coded R0. 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 R0 and H0; 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.

Relationship to existing work

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.

Licence and provenance

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.

Development

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.md

Tests 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.

About

PS3 RSX (NV47) vertex/fragment shader microcode to HLSL recompiler

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages