How a PlayStation 5 emulator actually works
Every hard idea in KytyPS5, shown moving. How guest code runs without a CPU emulator, how the address space is carved up, how a file becomes a running program, which thread does what, and how command packets become pixels. Each animation plays on its own and can be stepped by hand.
- Every animation plays while it is on screen and pauses when you scroll away, so nothing moves out of sight.
- Next and Back step it by hand — the caption under each one changes with the step, and that is where the actual explanation lives.
- If your system asks for reduced motion, nothing auto-plays; step through instead.
- Deeper on any topic: the book covers all of this in prose with real source code.
The whole machine, once through
Start here. Everything else on this page is one of these eight boxes opened up.
The thing to notice is that no step in that chain is a function call the emulator can intercept. The game writes command packets into memory it owns, then hands over a pointer. Everything after that is Kyty reading those packets and rebuilding the work in Vulkan.
The CPU — how guest code runs at all
The PS5's processor is an 8-core AMD Zen 2 running x86-64 — the same instruction set as your PC. That single fact reshapes the entire project.
So KytyPS5 is not a simulator that steps through the game. It is a host environment the game runs inside — closer in spirit to Wine than to Dolphin. The emulator only regains control when the game calls a function it provides, or when the hardware raises a fault.
The instructions match; the conventions do not
Machine code has no concept of a "function argument". Which register carries which parameter is a convention — and the PS5 and Windows disagree about almost all of it.
System V variadic calls carry the vector-register count in al. guestPrintf.cpp interprets guest format strings and arguments explicitly; passing that argument list to the host's vprintf would violate the calling convention.
Rewriting the guest's own instructions
Two instruction patterns in PS5 code cannot execute on Windows. With no recompiler in the pipeline, the loader rewrites the bytes in place — before the code is ever run.
You can watch this happen for real. Run any title, grep the log for Patch tls at addr, then look at those nine bytes in a debugger: the file on disk has 64 48 8B 04 25 00 00 00 00 and memory has 48 E8 … 48 89 C0. Seeing it once makes the whole mechanism concrete.
Faults are a control-flow mechanism
Because there is no code generation step, the emulator cannot instrument guest code. The CPU's own fault mechanism becomes the hook — and two of the handler's three branches are deliberate design.
Memory — carving up the address space
Console software hard-codes addresses and assumes allocations land in particular ranges. It also hands pointers to the GPU. So Kyty claims whole bands of the host address space up front and allocates inside them itself.
Three ways a game asks for memory, and Kyty models each separately: direct memory is a physical reservation that gets mapped as a second step; flexible memory comes from a fixed per-title budget; pooled memory is reserve-then-commit with a batch interface. On top sit sceKernelVirtualQuery — which forces real bookkeeping, since the guest can ask what protection, type, and name a range has — plus PRT apertures for sparse textures.
16 KB guest pages on 4 KB host pages
A mismatch that sounds like a footnote and turns out to shape the whole coherency design.
Keeping CPU and GPU copies in step
The hardest problem in the project. A console has unified memory, with guest ordering and cache operations. On a PC, every shared resource is a copy that can go stale — and nothing tells the emulator when it does.
The code is unusually defensive about this, and the reason is worth internalising: a coherency bug produces corruption many frames after its cause, with no traceable link. So each invariant becomes an immediate crash instead — re-entering the tracker from inside an upload callback is a hard failure, and an invalidation that leaves GPU-owned pages behind aborts on the spot.
Startup — a file becoming a running program
A PS5 executable is a SELF: a signed container wrapping standard ELF64, with Sony's own segment types and dynamic tags bolted on.
Wiring up the imports
PS5 modules do not export readable names — they export hashes. Resolution is where the emulator inserts itself into the game.
How a console call reaches emulator code
The single most important mechanism in the project, and it hinges on one pointer.
Before implementing an unresolved NID, establish its arguments, outputs, lifetime and error contract from evidence. Register a justified implementation and test ordinary, boundary and error behavior. Removing a stub message or returning OK does not prove the guest-visible behavior is correct. Follow the first-change lesson and contribution capstone. Two ways it silently fails: one wrong character in the NID string, or a LIB_VERSION whose library or module version does not match what the game imports.
Threads — who runs what
Three actors, three completely different jobs, and a separation that is required rather than stylistic.
Guest threads are real host threads, but with two pieces of state Kyty maintains itself: a guest stack allocated inside the guest bands, because guest code computes addresses relative to it and may hand them to the GPU; and a per-thread copy of every loaded module's TLS image, allocated lazily and freed through the game's own allocator if it installed one.
Fifty-seven GPU queues
One graphics queue plus seven compute pipes of eight, mirroring the hardware — because games rely on that topology for asynchronous compute.
The GPU — packets in, pixels out
The game never calls a draw function. It builds raw command packets — the same binary format a real AMD command processor reads — and submits the buffer. So Kyty implements a command processor in software.
Beyond register writes and draws, a real command processor has mechanisms Kyty has to model too: two engines (drawing and constant) synchronised by counters, a 48 KB constant-RAM scratch area, end-of-pipe fences, indirect buffers that work like calls, predication that makes packets conditional, and blocking waits that suspend a submission rather than spinning.
Waves, lanes, and why shaders are hard
A GPU does not run one shader invocation at a time. It runs 32 or 64 in lockstep on one instruction stream — which means branching cannot work the way you expect.
RDNA 2 machine code becoming SPIR-V
About 28,700 lines of compiler: explicit decode/CFG/translation frontends, one typed SSA-capable IR and its passes, then the SPIR-V backend.
Structurisation failure is a supported outcome. An irreducible graph, or a graph for which merge and continue blocks cannot be recovered, selects the dispatcher: one loop around a switch on a program-counter value, each basic block a case. Slower, but explicit and diagnosable.
Finding the texture a shader wants
On RDNA 2 there are no texture slots. A shader holds its resource descriptors in registers, or loads them from memory itself — so the emulator has to evaluate the shader's own address arithmetic before the shader ever runs.
Why textures are not stored in rows
Guest memory holds swizzled data; Vulkan wants something else. Converting between them is address arithmetic plus a compute shader per layout family.
Presentation — the flip model
A game does not present a frame. It registers buffers, renders into one, and asks the display to scan it out at the next vertical blank.
All nineteen, in one list
Every animation on this page, what it explains, and where the prose version lives.
| Animation | Explains | In the book |
|---|---|---|
| pipeline | Game code to pixels, eight stages | ch 3, 25 |
| nativeexec | Interpreter cost vs native execution | ch 1, 3 |
| abiflow | System V vs Microsoft x64, and the mismatch | ch 4 |
| patchbytes | The fs:[0] nine-byte rewrite | ch 16 |
| faultpath | The exception handler's three branches | ch 18 |
| addrmap | Address bands filling with allocations | ch 5, 19 |
| pagegrain | 16 KB guest pages over 4 KB host pages | ch 5 |
| coherency | Page faults as a notification channel | ch 32 |
| elfmap | Segments mapped, patched, protected | ch 13, 14 |
| relocgot | NIDs resolving into GOT slots | ch 15, 17 |
| gotplt | A console call landing in emulator C++ | ch 15, 22 |
| threads | Main, guest and GPU threads over time | ch 12, 20 |
| queuebank | 57 queues and a blocked submission | ch 25, 26 |
| pm4stream | Packets consumed, registers filling, draw | ch 26, 27 |
| wave | An if across 64 lanes via the EXEC mask | ch 7, 30 |
| shaderflow | Five forms from machine code to SPIR-V | ch 30 |
| srtwalk | User data → table → descriptor → binding | ch 28 |
| tiling | Memory order, linear vs tiled | ch 29 |
| flipmodel | Buffers, flip queue, vblank, scanout | ch 33 |
Written by reading the KytyPS5 source tree at main commit d2413fc2 (7 October 2026); source references use paths and symbols at that revision. These are unofficial — a reading of the source, not maintainer-authored documentation. Where a document and the code disagree, the code is right.
Some animations are deliberately simplified and say so in their own notes — the tiling one in particular shows 2×2 Morton ordering to make the idea visible, where the real families vary block size by bytes-per-element, XOR block coordinates across memory channels, and pack small mips into a tail.
KytyPS5 is GPL-2.0-only and based on InoriRus/Kyty (MIT), and credits shadPS4 as a reference for the memory model and AVPlayer. Not affiliated with Sony Interactive Entertainment.