Recompiling RDNA Shaders Without Losing Their Interface

A Vulkan pipeline can consume SPIR-V, but a PS5 title does not submit SPIR-V as its original shader representation. AnyPS5 must decode the guest's RDNA instructions, reconstruct control flow and resource use, and emit host code whose interface still matches the draw or dispatch that supplied it. This chapter treats the shader recompiler as a compiler rather than a text substitution, then separates reusable shader variants from the runtime data that changes on every invocation.

From instructions to an intermediate representation

A textual substitution would be enough only if each source instruction had one independent host instruction with the same inputs, outputs, control-flow behavior, resource model, and wave semantics. A shader rarely has that shape. Branches, loops, lane operations, resource descriptors, stage inputs and outputs, and target-specific capabilities interact across the whole program. AnyPS5 therefore begins by decoding RDNA instructions into a representation it can analyze rather than translating one line at a time.

The first structural stage builds a control-flow graph and structurizes it. Blocks and branch edges tell the compiler which instructions can execute, where values may merge, and which regions represent loops or conditional paths. This information is needed before optimization and emission. A translator that preserved arithmetic operations but lost the conditions under which they run could produce a valid SPIR-V module with different behavior.

The instruction translator then maps decoded operations into an intermediate representation. The IR gives later passes a common language for values, blocks, resources, and stage-specific operations. SSA construction makes value definitions explicit: a value has a controlled set of producers, and merges at control-flow joins can be represented deliberately instead of hidden in mutable host variables. Constant folding can resolve known expressions, identity cleanup can remove redundant operations, and dead-code elimination can discard work that no remaining output or resource use depends on.

These passes are not cosmetic. They create the information needed to translate the guest program into a host shader without carrying every source-level accident into the output. They also expose unsupported cases. If the control-flow structure, resource chain, or instruction semantics cannot be represented safely, the compiler can throw rather than emitting a shader whose pixels or writes are silently wrong.

Control-flow analysis also establishes where values are live. A value produced in one block may be consumed only on a particular branch, or it may need a merged definition at a loop exit. The IR and SSA passes make those relationships visible to later resource and output analysis. They can remove an identity operation without removing a value that still feeds a stage output, and they can eliminate dead work only after checking that it has no observable memory, image, or synchronization effect.

The compiler's optimization choices are therefore tied to the guest shader's observable behavior. Constant folding is safe when the inputs are known under the compiler's model. Dead-code elimination is safe when the removed operation has no remaining output or resource effect. A resource tracker must preserve an access that looks unused to ordinary arithmetic analysis if it writes a guest buffer or participates in an atomic operation. This is why a shader recompiler needs a semantic IR: the host target must receive the consequences of the guest program, not only its visible arithmetic expressions.