2026-10-08 · 30 min · gpu · compilers
Why read this
Notabletop 60%Code-level teardown of AnyPS5: how a PS5 ELF becomes a native Linux program, NIDs recomputed, the GPU path traced, and how it differs from shadPS4 and recomps.
- Interactive explanations
- Original analysis
- Runs on a consumer GPU
GPUs, kernels & systemsGPL-2.0-onlyPractitioner tool
How this was scored
- Is it new?
- 2 of 3: A real new idea, method or capability
- Can I trust it?
- 2 of 3: Measures key facts from files, code or configs
- Can I run it?
- 2 of 3: Open code or weights with real limits
- Will I understand it?
- 3 of 3: Mechanism carried by interactives built from real code or data
- Can I act on it?
- 1 of 3: General advice
- Will it last?
- 2 of 3: A reference for a year or more
- Does it affect many?
- 1 of 3: A specialist community
- Only here?
- 2 of 3: A teardown or measurement few others did
Score 66 of 100, ranked 165 of 476 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored
The README of AnyPS5 is two sentences long before it gets to the badges. It calls itself a "tool for automatic executables porting to Linux and Windows", with "a relinker that converts executable to the target system's native format and implementations of system prx libraries suitable for dynamic linking. No emulation or separate runtime process."
I read that as marketing. Every console project I know of either emulates the CPU, recompiles it into C, or, in the case of shadPS4 on the PS4, runs the game's x86-64 code natively inside an emulator process that loads and patches it at run time. "No separate runtime process" sounded like one of those wearing a different hat.
It isn't. The output of AnyPS5's relinker is a file that /lib64/ld-linux-x86-64.so.2 loads like any other program. glibc's dynamic loader binds the game's imports to a directory of .prx files, which are ordinary shared libraries whose exported symbols have been renamed to the console's hash names. Nothing interprets the game's instructions or rewrites them in memory. On Linux the CPU code is not even touched. The part of AnyPS5 that does look like an emulator is the graphics driver, 102,000 lines of C++ that parse the GPU's command packets and recompile its shaders to SPIR-V. That part deserves its own section, and it gets one.
- license
- GPL-2.0
- branch
- main
- tests
- 535 files
- source
- 14.6 MB
- commit date
- 2026-10-09
by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded
local clone, 2026-10-09 at 8b393b6 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount
shallow clone: counts describe the pinned tree, not the history
Where it stands
I cloned boykopovar/AnyPS5 at 4b6ab6b, the merge of pull request #1797 at 21:40 CEST on 8 October 2026. The tree is 1,687 tracked files and 23 MB. The system libraries under core/libs/prx are 172,064 lines of C++ in 115 directories, the graphics driver libSceAgcDriver alone is 101,977 of them, the shader recompiler is 50,592 and the relinker 16,064. The GitHub API counts 14,760 stars and 1,173 forks.
The progress page, which a workflow regenerates on every push, rendered like this when I loaded it:

Both badges need reading carefully, and the project says as much. The library figure, 84.76%, is 2,575 of 3,038 functions declared in the repository: a function counts as done when its body does not call the stub NotImplemented_nid_no_patch. It is not a fraction of everything the PS5 firmware exports, and the footnote under the badge says so. I recounted it my own way, with a regex over every APS5_VABI definition, and got 3,052 functions and 429 direct stub calls, close enough that the badge is what it says it is.
The shader figure, 98.37%, is weaker than it looks. tools/progress.py counts an RDNA instruction as implemented "when the decoder recognizes it". Decoding an opcode and translating it correctly are different jobs, and the 345-line technical-debt file is full of instructions that decode fine and are translated with caveats. I would read that badge as "the decoder is nearly complete".
Update, 9 October. Two days and about 150 commits later the shader badge reads 100% (1,166 of 1,166), and people started saying the project was done. The definition did not change: tools/progress.py:284 still counts an instruction once the decoder recognizes it, so the badge now says the decoder is complete. I checked the next step myself. Of the 1,085 opcodes in RdnaOpcode, 1,078 are referenced somewhere in the translator outside the decoder, which is a better sign than the badge, though a reference is not a correct lowering. The seven that are not include the two typed-buffer loads TBufferLoadFormatX and TBufferLoadFormatXyzw, the Exp export family and ImageGather8hPck, and the translator still throws "not implemented" on some compare and attribute paths and on any tessellation setup other than one fixed configuration (ShaderInputInfoBuilder.cpp:177). The library badge barely moved, 2,578 of 3,044 (84.69%), slightly lower than before because more functions were declared. The compatibility list still has one game.
The compatibility list has one row. Dreaming Sarah, a 2D platformer (PPSA02929), is "In game, playable" on Windows, with a question mark under Linux, at 60 FPS on a GTX 1050 Ti with an i5-7500 and 36 FPS on Intel HD Graphics 620 with an i5-7200. The screenshot the author posted to the project's documentation gist is a Windows window:

One game in two months sounds thin. I think it is the right first game: a small 2D title exercises the loader, the core libraries and the GPU path end to end without needing every corner of any of them.
What a PS5 executable is
The PS5 runs a FreeBSD-derived kernel on an eight-core AMD Zen 2 CPU, and its executables are ELF files with Sony's extensions. On the console they ship inside a signed, encrypted container (SELF). AnyPS5's usage document asks for "a clean ELF executable" and does not discuss containers, keys or where the ELF comes from, and neither will I. Everything below is about the ELF inside.
Three things make it different from a Linux program.
Tables the loader reads but never maps
A Linux executable's PT_DYNAMIC segment points at its symbol table, string table and relocations by virtual address. In a PS5 ELF the same tags exist in an OS-specific range (DT_OS_STRTAB is 0x61000035, DT_OS_SYMTAB is 0x61000039, and so on), and their values are file offsets into a segment of type PT_SCE_DYNLIBDATA (0x61000000) that the loader reads but does not map. The relinker handles both forms in one helper:
// core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp:101
auto readAsOffset = [&](const std::int64_t osTag, const std::int64_t sysvTag, const char* name) -> FileByteOffset {
if (requireExactlyOneOf(osTag, sysvTag, name))
return getTagValue(osTag);
return _elfReader->TranslateVirtualAddress(getTagValue(sysvTag));
};Imports named by hash
A PS5 game does not import sceVideoOutOpen. It imports Up36PTk687E, an 11-character NID ("name ID"), followed by # and a library id and # and a module id. The NID is the first 8 bytes of SHA-1 over the function name followed by a fixed 16-byte suffix, read little-endian and written as 11 characters of a base64 alphabet that uses + and -. AnyPS5's build tool computes it like this:
// core/libs/nid/src/NidCompute.cpp:8
constexpr uint8_t kNidSuffix[16] = {
0x51, 0x8D, 0x64, 0xA6, 0x35, 0xDE, 0xD8, 0xC1,
0xE6, 0xB0, 0x39, 0xB1, 0xC3, 0xE5, 0x52, 0x30
};
...
const auto digest = Sha1(input); // line 27: name bytes + kNidSuffix
uint8_t reversed[8];
for (int i = 0; i < 8; i++)
reversed[i] = digest[7 - i];I reimplemented it in Python and in TypeScript (the TypeScript one runs in the widget further down) and hashed five names. sceKernelUsleep gives 1jfXLRVzisc, sceVideoOutOpen gives Up36PTk687E, scePadRead gives q1cHNfGycLI and sceKernelAllocateDirectMemory gives rTXw65xmLIA. All four match the NIDs hard-coded in shadPS4's LIB_FUNCTION tables at its 7 October HEAD, and sceAgcDriverSubmitDcb gives UglJIZjGssM, which matches shadPS4's generated name table. The function is the same one the PS4 used. That matters for everything that follows, because it means a host library can produce the console's names from readable C++ function names at build time, with no table of names shipped at all.
The module id after the second # is decoded with the same alphabet and looked up in another OS-specific tag, 0x61000045, whose value packs a module id in the top 16 bits and a string offset in the low 32 (RelinkerPipeline.cpp:189). That is how Up36PTk687E#…#… becomes "this comes from libSceVideoOut.prx".
A syscall boundary inside a library
On the console, a game does not issue system calls. It calls libkernel.prx, and libkernel issues the syscall instructions into the FreeBSD-derived kernel. For AnyPS5 that is good news: if libkernel is reimplemented, there is no kernel interface to emulate. The relinker turns that assumption into a hard check. It linear-sweeps every executable segment with its own x86-64 length decoder and refuses the file if any instruction is a syscall, int 0x80, sysenter or sysret:
// core/relinker/relinker/src/analysis/SyscallScanner.cpp:41
const bool isSyscall = (b0 == SYSCALL_BYTE0 && b1 == SYSCALL_BYTE1);
const bool isInt80 = (b0 == INT80_BYTE0 && b1 == INT80_BYTE1);
const bool isSysenter = (b0 == SYSENTER_BYTE0 && b1 == SYSENTER_BYTE1);
const bool isSysret = (b0 == SYSRET_BYTE0 && b1 == SYSRET_BYTE1);A game that inlines its own system calls, which some middleware does on other platforms, is rejected at conversion rather than crashing later. The --skip-syscall-check flag that turns this off is marked deprecated and "extremely unstable".
A game also ships its own modules beside the executable, in sce_module/ (or sce_modules/, or prx/): the C runtime and middleware the developer linked dynamically. These are the same kind of ELF, and AnyPS5 converts them too, rather than reimplementing them. CONTRIBUTING.md is explicit that engine and middleware modules a title ships, such as FMOD or Cohtml, are never replaced: "The relinker converts and loads the title's own module". Only libraries that reach the kernel or the hardware are rewritten.
Relinking: the code stays, the tables change
Here is the step that I did not believe until I read it. The machine code of a PS5 game is already x86-64 for an AMD Zen 2. A Linux or Windows PC on x86-64 executes the same instructions. What stops the game from running on a PC is everything around the instructions: who loads the file, who fills in the addresses of imported functions, how the entry point is called, where thread-local storage lives, and what calling convention the libraries use. Change those, and the instructions can stay as they are.
The project's own architecture document draws the whole flow:

main.cpp runs it in that order. RelinkerPipeline::Relink reads every relocation in the console's .rela.dyn and .rela.plt tables. Relative relocations, which only add the load base, are copied over unchanged. Every relocation that names a symbol becomes a NidReference: the NID string, the library it resolved to, the relocation type, and the address it patches, which is a slot in the game's global offset table. A relocation type outside a list of 14 standard x86-64 ones fails validation (ValidationPolicy.cpp:18).
SysVDynamicSectionBuilder then writes those references back out as a standard SysV import: one symbol per reference, named by the NID with everything from the first # cut off, and one relocation of the same type at the same GOT address. It is careful about one thing. The game's PLT stubs each have the index of their .rela.plt entry compiled into them, so jump-slot relocations must come out in their original order, and the builder throws if one is missing rather than renumber them (SysVDynamicSectionBuilder.cpp:128).
LinuxElfPatcher::Patch assembles the file. Click through it:
mov rdi, rsp; and rsp, -16; xor rsi, rsi; call entry; ud2. Linux starts a process with argc and argv on the stack in the same shape the console's entry expects behind its first argument, so a pointer to the stack is the whole adapter.
The original bytes are copied untouched, and everything new is appended at the end of the file and mapped by one new PT_LOAD: the string table, the symbol table, both relocation tables, a new PT_DYNAMIC with DT_NEEDED for every system library, DT_RUNPATH of $ORIGIN/libs and DF_BIND_NOW, a 17-byte entry stub and the name of the interpreter. The header is changed to ET_DYN with OS ABI 0, so Linux sees a position-independent executable and places it wherever ASLR likes; the copied relative relocations take care of the rest. The Sony dynamic segment and PT_SCE_DYNLIBDATA lose their program headers and are simply never mapped (SegmentFilter.cpp:13).
The entry stub is my favourite part of the codebase, because it is so small:
// core/relinker/elfpatcher/src/general/EntryStubBuilder.cpp:18
_appendBytes(s, kStubOpMovRdiRsp, sizeof(kStubOpMovRdiRsp)); // mov rdi, rsp
_appendBytes(s, kStubOpAndRsp0xf0, sizeof(kStubOpAndRsp0xf0)); // and rsp, -16
_appendBytes(s, kStubOpXorRsiRsi, sizeof(kStubOpXorRsiRsi)); // xor rsi, rsi
...
s.push_back(kStubOpCallRel32); // call <original e_entry>
...
_appendBytes(s, kStubOpUd2, sizeof(kStubOpUd2)); // ud2The console's entry point takes a pointer to an argument block and an exit handler. shadPS4 builds that block by hand as a struct of int argc, 4 bytes of padding and an array of argv pointers (src/core/linker.h:49). Linux starts every process with a 64-bit argc followed by the argv pointers on the stack. Little-endian, those are the same bytes. So mov rdi, rsp hands the game a valid argument block, and a null exit handler goes in rsi. The project has a test that runs a relinked fixture with arguments and checks argc and argv[0] from inside it (test_linux_entry_argv.py).
What has to change, and what doesn't
Going through the ABI list one item at a time shows how much of the work the Linux host does for free.
The calling convention is SysV AMD64 on both sides. The macro every exported function carries, APS5_VABI, is empty on Linux.
Thread-local storage on x86-64 FreeBSD and on glibc both use the "variant II" layout, with the thread's TLS block below the address in the fs segment register. The relinker keeps the game's PT_TLS segment as it is, and glibc's loader allocates the block for every thread. The code does nothing special on Linux, and the tests that cover TLS are about Windows, which is where the trouble is.
Memory layout is handled one level up. The console gives games "direct memory" that the CPU and GPU share, mapped at addresses the game picks. AnyPS5's libkernel backs it with one 13,824 MiB region, a memfd_create file on Linux and CreateFileMappingW on Windows (libkernel/DirectMemory/DirectMemory.cpp:229 and :237), and maps pieces of it with MAP_SHARED | MAP_FIXED where the game asks. The usage notes warn that Windows commits the whole allocation the moment a title makes it, so the page file has to cover it.
Syscalls never reach the host kernel unmediated, by the check above.
The instruction set is the one real exception. Zen 2 has instructions that Intel CPUs do not: SSE4a's EXTRQ, INSERTQ, MOVNTSS and MOVNTSD, CLZERO, MONITORX/MWAITX, the SHA extensions on Intel parts that lack them, and RDPRU and MCOMMIT. On an AMD PC none of this matters. For Intel hosts there is --to-intel, which decodes every code segment, rewrites each such instruction in place when a same-length replacement exists, and otherwise overwrites it with a 5-byte jump to an out-of-line stub that moves the stack below the 128-byte red zone, does the work, and jumps back (LinuxElfPatcher.cpp:32, Amd64OnlySubstitutionTable.hpp:21). An instruction shorter than 5 bytes with no in-place form is "kept: no room for a jump" and reported. RDPRU and MCOMMIT are not lowered at all, and fail the relink.
One detail in there tells you how carefully the project treats fidelity. VRCPPS and VRSQRTPS compute approximate reciprocals, and AMD and Intel approximate them differently, so the same game computes slightly different floats on each. --to-intel replaces them with correctly rounded results, and the technical-debt file states that this is neither AMD's answer nor Intel's. That is the honest choice, and the file is where it belongs.
The libraries on the other side
Each directory under core/libs/prx builds one shared library with a .prx suffix. On Linux that is an ELF .so, on Windows a DLL. Functions are ordinary C++ with C linkage and their real names:
// core/libs/prx/libSceVideoOut/src/Output.cpp:54
int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param) try {
validateOpenParam(param);
if (userId != 255 && userId != 0) {
throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE");
}After the link, nid_patcher walks the export table and renames each symbol to its NID. A handful of suffixes steer it. A name ending in _nid_postfix has the suffix stripped before hashing, so sceKernelUsleep_nid_postfix exports the NID of sceKernelUsleep, and mprotect_nid_postfix exports the NID of mprotect without colliding with the host C library's own mprotect at link time. A name ending in _nid_no_patch is a helper that stays a plain name. APS5_EXPORT("<nid>", func) sets the NID by hand for functions whose original names nobody knows, through an assembler alias:
// core/libs/prx/libc/include/general/ExportMacros.hpp:5
#define APS5_EXPORT(exportName, funcName) \
__asm__(".globl \"" exportName "_nid_no_patch_cut\"\n\t" \
".set \"" exportName "_nid_no_patch_cut\", " #funcName)So "implementing a system library" here means writing C++ against the console's documented or reverse-engineered behaviour, and the build turns the result into something the converted game's imports bind to. Follow one call through every layer:
Compiled game code never knows where sceVideoOutOpen lives. It calls a stub in its procedure linkage table, which jumps through a slot in the global offset table. On the console, the system's loader fills that slot. The relinker does not touch this code at all.
call sceVideoOutOpen@plt ; schematic, not from a title sceVideoOutOpen@plt: jmp *GOT[n](%rip) ; slot n, filled at load time
Two policies run through the 172,000 lines, and both are stated in CONTRIBUTING.md.
The first is "Every function either does exactly what it is supposed to or throws." An unimplemented export calls NotImplemented_nid_no_patch(__func__), which throws std::runtime_error with the function's name (libc/src/General.cpp:314), and the process prints it and ends. Even invalid arguments that the console would answer with an error code throw here, as in sceVideoOutOpen above, on the theory that a title passing them means AnyPS5 got something else wrong first. Exceptions are allowed only as "silent stubs" that unblock a title and only affect UI, and every one is listed by name in the technical-debt file: the message dialog finishes at once and answers button 1, the save-data dialog shows nothing, the login dialog reports that the user cancelled.
Compare shadPS4, the PS4 emulator this project borrows struct layouts from and credits in 20 of its files. At its 7 October HEAD, shadPS4's library tree has 2,947 log lines that start with (STUBBED), almost all in functions that log and return success. That is a pragmatic choice that gets more games to boot, and it is the opposite of AnyPS5's. A crash with the function's name in it is easier to fix than a game that runs on with a fake success and breaks three systems later. It also means AnyPS5 will look worse than an emulator of the same maturity for a long time.
The second policy is that missing imports fail at startup. With DF_BIND_NOW the loader resolves every NID before the first instruction of the game runs, and tools/import_audit.py will tell you before that, from the relinker's --registry output and the built .prx files, which imports are implemented, which are throwing stubs and which are absent. An unused-filter option can drop imports that a control-flow analysis proves unreachable, so that a never-called function nobody has declared does not block the load. It is off by default.
Windows is where the work is
Everything above was the Linux path, where the host happens to agree with the console about almost everything. Windows agrees about the instruction set and nothing else, and the relinker's Windows half is where most of its cleverness has gone.
The calling convention differs: the Microsoft x64 convention passes the first four arguments in rcx, rdx, r8 and r9, SysV in rdi, rsi, rdx, rcx, r8 and r9. So on Windows APS5_VABI expands to __attribute__((sysv_abi)) (libc/include/general/VabiMacros.hpp:4), every export uses the game's convention, and variadic functions such as printf use GCC's __builtin_sysv_va_list (libc/src/FormattingWide.cpp:17). That is one reason the Windows build needs GCC, a specific MinGW-w64 15.2.0 build, rather than MSVC, which has no sysv_abi.
Thread-local storage differs completely. Windows keeps its thread block in gs and its TLS slots in an array reached through gs:[0x58], and nothing on Windows sets fs to anything a game can use. So WindowsTlsBuilder finds every instruction in the game with an fs segment prefix, replaces it with a jump to a generated stub that loads the thread's TLS pointer from gs:[0x58], indexes it with the module's TLS index and redoes the access against that block, then jumps back. Instructions shorter than the 5-byte jump drag the following instruction into the stub with them. Only displacement 0 (the thread pointer itself), offsets inside the TLS block and fs:0x28, the stack-protector canary, are allowed. Anything else stops the conversion with the displacement in the message (WindowsTlsBuilder.cpp, from line 120).
Dynamic linking differs too. An ELF symbol is looked up in every needed library in order; a PE import names its DLL. Rather than translate one model into the other, the Windows output imports only 22 kernel32 functions, and the relinker emits an entry stub in raw machine code that calls LoadLibraryExA on each needed .prx, finds every NID, writes the GOT slots and then calls the game's entry point. In effect each converted Windows game carries a tiny dynamic loader of its own. The import audit flags the one case where the two models disagree: an import that names one library but is exported only by another resolves on Linux and can fail on Windows.
The GPU: 55 packet types and a shader compiler
If the CPU side is a linker, the GPU side is a driver. The PS5's GPU is an AMD RDNA 2 part, gfx1013 per the project's hardware-oracle notes, and games drive it the way AMD's own drivers do: they write PM4 packets, the command processor's native format, into command buffers in ordinary memory, and submit them. AnyPS5's architecture document draws its version:

The game builds its command buffers with libSceAgc, a user-mode library that AnyPS5 also reimplements, and submits them with sceAgcDriverSubmitDcb. That function is three lines: it hands the packet to AgcDriver::Submit on queue 0. A "GPU address" here is just a pointer into the process, because on the console CPU and GPU share one address space and AnyPS5 keeps it that way.
The parser reads each packet's header the way the hardware does: type 3 in the top two bits, a payload length in bits 16 to 29, an opcode in bits 8 to 15.
// core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp:226
require((header & 0xc0000000u) == 0xc0000000u, "unsupported PM4 packet type");
require(packet.size() == ((header >> 16u) & 0x3fffu) + 2u, "invalid PM4 packet size");
const auto opcode = (header >> 8u) & 0xffu;Pm4Opcodes.hpp names 55 opcodes, from SET_SH_REG and SET_CONTEXT_REG, which load GPU state registers, to DRAW_INDEX_2, DISPATCH_DIRECT, WAIT_REG_MEM and RELEASE_MEM. Sony's own commands, such as FLIP and WAIT_FLIP_DONE, hide inside NOP packets with a sub-opcode in bits 2 to 7. Every opcode without a handler has a reason string, and the validator throws it: "nested command buffers, branching and nested flip reservation are not implemented", or, best of all, "GPU LOD statistics are not implemented; synthetic results are forbidden". The same rule as the libraries, applied to a GPU.
Memory is the expensive part. A Vulkan GPU cannot just read a pointer into the game's heap, so buffers and textures have to be mirrored into device memory, and kept in step when the CPU writes them. Linux imports guest memory through udmabuf where it can, Windows uses VK_EXT_external_memory_host and the GetWriteWatch API to find written pages, and the executable's own image, which Windows refuses to import, gets a "mirror" whose writable parts are compared 64 KiB at a time (Graphics/src/GuestBufferMemory.cpp:41). This is the same problem every console emulator with unified memory has, and there is no linker trick for it.
RDNA in, SPIR-V out
Shaders are where "native" stops completely. The game's shaders are RDNA machine code. A PC GPU from NVIDIA or Intel cannot run them, and even an AMD PC GPU cannot be handed raw ISA through Vulkan. So each shader the game binds is recompiled. Recompiler.cpp runs the stages in order: RdnaDecoder turns the dword stream into instructions; GraphBuilder builds a control-flow graph; Structurizer, 1,602 lines on its own, turns RDNA's arbitrary branches into the structured selections and loops SPIR-V requires; InstructionTranslator lowers RDNA to an intermediate representation; SSA construction, constant folding, dead-code elimination and two lane-specific passes clean it up; SrtWalker and ResourceTracker work out which resource descriptors the shader reads from memory so they can become Vulkan bindings; and SpirvBackend emits the module.
The interesting translation problem is the wave. An RDNA shader runs 32 or 64 lanes in lockstep, and much of its control flow is written as scalar operations on an EXEC bitmask: a branch is "clear the bits of the lanes that go the other way". SPIR-V has no such register. AnyPS5 models EXEC as a per-invocation boolean, and when a shader reads a lane mask as a value, it rebuilds it with a subgroup ballot:
// core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp:8
std::array<IrU32, 2> TranslationContext::ballotMask(IrU1 value) {
IrValue& mask = ir.Emit(IrOpcode::Ballot, IrType::U32x4, {&value.Value()});
return {IrU32(ir.CompositeExtract(mask, 0u)), program.WaveSize() == 64u ? IrU32(ir.CompositeExtract(mask, 1u)) : IrU32(ir.Constant(0u))};
}That works exactly when the host's subgroup is as wide as the console's wave. On an NVIDIA GPU, whose subgroup is 32 lanes, a wave64 shader that reads EXEC as a value, or reads another lane with v_readlane_b32, sees only the lanes its subgroup holds. The technical-debt file lists each such case rather than hiding it. Compiled variants are cached in memory and on disk, keyed by the code and the state that specialises it, and the debt file notes that recompiling at draw time "should be moved to the relinker stage". That would be the link-time idea carried to the GPU, and it has not happened yet. The GTA V browser port shows what that looks like when it is done: there, every compiled Direct3D shader was translated to WGSL ahead of time and shipped in packs, and the run-time layer only replays commands.
The most unusual thing in this half of the repository is tools/hw-oracle. It runs a few lines of RDNA assembly on a local AMD GPU through ROCm's HSA runtime, one input row per lane, and prints what the hardware computed, with every floating-point mode bit set explicitly. Contributors are required to say where an instruction's semantics came from, a named GPU measured with the oracle or an exact section of the ISA, LLVM or Mesa. Many entries in the debt file read "measured on RDNA2 (gfx1035)", a laptop iGPU of the same family as the console's gfx1013. Most emulator shader translators are written from documentation and fixed when a game looks wrong. Measuring first is slower and much better.
Is it really not an emulator?
For the CPU, yes, and more strictly than for shadPS4. shadPS4 also executes the game's x86-64 instructions directly on the host CPU. The difference is where the loading happens. shadPS4 is a program that maps the game's ELF into its own address space, applies relocations, looks every import up in a table of host functions built from LIB_FUNCTION(nid, library, version, module, function) entries, patches fs accesses on Windows and macOS in memory with Zydis and Xbyak, and then jumps to the entry point from inside its own process. AnyPS5 does each of those steps once, ahead of time, and writes the result to disk, where the operating system's loader takes over.
That buys real things. A converted game is an ordinary process: ldd lists its libraries, gdb and perf work on it with the NIDs as symbol names, and a library change is a rebuild of one .so. Every patch is visible in a file you can diff. Missing functions fail at load, or before it with the import audit, instead of whenever a level first calls them. And there is no emulator core to start, configure or keep running.
It also costs things. Each title has to be converted, and the conversion has to be redone when the relinker changes. Modules a game loads at run time with sceKernelLoadStartModule still need a loader inside libkernel, so the run-time half does not vanish entirely. On Windows the "OS loader" is a stub the relinker wrote, which is a loader by another name.
For the GPU, no. libSceAgcDriver interprets a hardware command stream and translates machine code for one GPU into an intermediate language for another, which is what an emulator's GPU core does. That is not a criticism. There is no other way to run RDNA command buffers on an NVIDIA card, and this one is unusually disciplined about it. But "no emulation" is true of the half of the system that was always going to be cheap. The 102,000 lines are where the cost is, and they are emulation.
The comparison with static recompilation is the more interesting one. N64Recomp turns a Nintendo 64 game's MIPS code into C, which is compiled into a new PC program, and XenonRecomp does the same for Xbox 360 PowerPC code. Those projects behind Zelda 64: Recompiled and Unleashed Recompiled have to translate every instruction because the target CPU is different. AnyPS5's target CPU is the same, so its "recompilation" of the CPU degenerates into relinking, and that is why it can be generic. A recomp is a per-game project with per-game source output, function boundaries discovered by hand and per-game patches. AnyPS5's relinker takes any title that passes its checks. What the two share is the graphics answer: a recomp's renderer, like RT64 or Unleashed Recompiled's shader recompilation, is the same kind of translation layer AnyPS5's driver is, and in both cases it is where most of the work and most of the bugs live.
| shadPS4 (PS4) | AnyPS5 (PS5) | N64Recomp / XenonRecomp | |
|---|---|---|---|
| Game CPU code | runs natively, loaded by the emulator | runs natively, loaded by the OS | translated to C or C++, recompiled |
| Imports bound | at run time, from an HLE table | at load time, by the OS loader | at build time, into the generated source |
| Unimplemented calls | often logged and stubbed | throw and end the process | per-game work |
| Per-game effort | none, plus fixes | a conversion run | a project per game |
| GPU | command stream and shaders translated at run time | command stream and shaders translated at run time, cached | a renderer per console |
How old and how busy
Fetching the full history turned up 3,001 commits on main. The first, "initial commit" by boykopovar, is dated 3 August 2026, and GitHub says the repository was created that evening. So the project is about two months old. Versions 0.1.0 and 0.1.1 were released on 28 September, with prebuilt relinkers and libraries for Linux and Windows. The pace since then is hard to believe: 221 commits on 4 October, 385 on 5 October and 446 on 8 October, the day I cloned it, counting merges. 121 author names appear in the log. boykopovar has 771 commits, and a handful of regulars have between 50 and 540 each.
A lot of it is machine-assisted, and the project asks contributors to say so. Of 1,939 non-merge commits, 787 carry a Co-Authored-By trailer naming Claude or Copilot. That is a floor, since disclosure can also go in the pull request text. It is visible in the house style: strict rules, a convention checker that rejects code comments unless the technical-debt file changes in the same pull request, a 30-second budget per test, and documentation written in the same dry, complete sentences throughout. I would not draw conclusions about code quality from the trailers alone. The rules the project enforces on its contributors, human or not, are the more useful signal, and they are good rules.
The legal line
The README's disclaimer, in full:
This project is intended for interoperability, research, preservation, and compatibility purposes. It does not include, distribute, or require copyrighted software, firmware, cryptographic keys, or proprietary libraries. Users are responsible for ensuring that any binaries used with this project are obtained and used in accordance with applicable laws and their respective license terms.
That is the standard position of console emulation and porting projects, and it rests on a few well-known pillars. Reimplementing an interface so that independently created software can run is generally lawful: in the United States, Sony v. Connectix (2000) held that copying the PlayStation BIOS during reverse engineering to build a compatible emulator was fair use, and the EU Software Directive permits decompilation for interoperability under conditions. What projects avoid is shipping the vendor's code, firmware or keys, and anything that circumvents copy protection, which is where anti-circumvention law such as DMCA section 1201 applies independently of copyright in the game. AnyPS5 ships its own C++ for every system library, generates NIDs from names rather than shipping a NID table (tools/nid_names.py notes the name list is "Never downloaded"), and does not handle decryption at all: its input is a plain ELF. Where a game asks for the console's system fonts, it falls back to openly licensed Noto fonts.
The usual risks are elsewhere: trademarks (the project's name invokes one), contributors who have seen leaked SDK material, and users, who are responsible for where their executables come from. None of that is something code review can settle, and I am not offering legal advice. The code I read stays on the interoperability side of the line.
What I make of it
The relinker is the idea worth stealing. Once the guest CPU and the host CPU agree, an emulator's loader is a linker that runs every time, and AnyPS5 makes it a linker that runs once. The rest follows from that: the OS loader binds imports, the debugger sees a normal process, and failure is moved to the earliest moment it can happen. I expect someone to apply the same move to other x86-64 platforms whose binaries are ELF with hashed imports.
I would not judge the project by its compatibility list yet. One playable 2D game says that the loader, the core libraries and a slice of the GPU path work end to end, nothing more. What I would watch is the technical-debt file, which is the most honest progress report in the repository, and the shader recompiler's move to conversion time, which would let the GPU half follow the CPU half and fail early too.
How I checked
I shallow-cloned boykopovar/AnyPS5 at 4b6ab6b and read the documentation under docs/, the relinker (core/relinker, all of the pipeline, the Linux and Windows patchers, the entry, TLS and dependency stubs and the AMD-only instruction converter), the NID patcher, a sample of the system libraries, the PM4 parser and the shader recompiler's entry point and EXEC-mask translation. File and line references are to that commit. I did not build or run anything from the repository. The NID function I reimplemented in Python and in TypeScript from NidCompute.cpp, and I checked five hashes against a shallow clone of shadps4-emu/shadPS4 at its 7 October 2026 HEAD; the same clone gave the (STUBBED) count and the EntryParams layout. Line counts are wc -l over .cpp and .hpp files. The library recount is a regex over APS5_VABI definitions, not the project's own scanner. History figures come from fetching the full commit history (git fetch --unshallow) and counting with git log; stars, forks and release dates come from the GitHub API on 8 October. The progress page, its SVG map and the two Mermaid diagrams were rendered in a headless Chromium; the Dreaming Sarah screenshot is the author's own, from the gist the documentation links to. I could not check the compatibility list's frame rates.