Drive the emulator's hard parts yourself
The animations elsewhere show you a fixed story. These do not: every tool here takes your input and computes a real answer. Type an address and watch it decompose. Click bytes and watch pages fault. Run a shader across 64 lanes and step it instruction by instruction. Build a command buffer and find out why your draw produced nothing.
- Real computation, not scripted playback. The address walker uses the actual band constants; the register decoder uses the actual shifts and masks from
pm4.h; the wave simulator is a working interpreter for a slice of the RDNA 2 instruction set. - You can break things on purpose. Draw without setting a render target. Exhaust the flexible-memory budget. Read registers with the wrong ABI. The error you get back is the one the emulator would give.
- Every tool has a ▶ Run demo button. Press it and the tool drives itself through a worked scenario, narrating each step — including the failure cases. Then the controls are yours.
- Nothing else is on a timer, so once a demo finishes nothing moves until you move it.
Step a guest function call
The PS5's CPU is x86-64, the same as your PC, so guest code needs no translation. What it does need is agreement about which register holds which argument — and that is where the emulator has to intervene.
- Run it once with the callee set to System V. All six arguments arrive correctly — this is what
KYTY_SYSV_ABIbuys. - Now switch to Microsoft x64 without resetting. Nothing about the register file changed, but
handleis suddenly thebufferpointer and two arguments were never passed at all. That is what a missing ABI attribute looks like. - Change an argument value and watch which register it lands in.
Translate a guest address
Kyty claims whole bands of the host address space and allocates inside them itself. Feed it any address and see which band owns it, which module it falls in, and how it splits into a page index and an offset.
- Start at
0x900012F40— insideeboot.bin. Add0x10000000and you land in the next module, because that is the spacing the loader uses. - Watch the bit ruler as you change the last few hex digits: only the low 14 bits move, because a guest page is
0x4000bytes. - Try
0x700000001234. That is the emulator's own image — deliberately linked far above anything the guest can allocate, so a guest pointer and a host pointer can never be confused by value.
Allocate guest memory
Three allocation models, because games use all three, and each behaves differently when it runs out.
- Allocate several flexible blocks until the budget bar fills. The failure you get is the real one — flexible memory comes from a fixed per-title budget declared in
param.json, not from whatever is free. - Fragment it: allocate five 64 MB blocks, free the second and fourth, then try to allocate 128 MB. There is plenty of free space and it still fails, because no single hole is big enough.
- Allocate direct memory and select it. Note it has a physical offset and a virtual address — two separate steps on this hardware, and one physical range can be mapped twice.
Host and guest memory, kept in step
The hardest problem in the project. On a console CPU and GPU share one pool of memory; on a PC the Vulkan resource is a copy, and nothing in the game's code says when it goes stale.
- Click a few bytes in different pages, then press Draw. Only the pages you touched are copied — read the log to see how many it found.
- After the upload every page is write-protected. Click a byte again: you get a fault, and the log shows the handler marking the page dirty and lifting protection so the write can complete. The game never learns any of this happened.
- Press GPU writes a render target, then CPU reads it back. That is the download path — the same machinery running the other way.
- Notice that clicking one byte dirties all eight in its row. Granularity is the 16 KB guest page, which is correct but conservative.
Decode a hardware register
Most PM4 packets are register writes, and a register is a packed bundle of bit fields. Unpacking them into something typed is a major responsibility of pm4Handlers.cpp.
- Click individual bits in the ruler and watch a named field change meaning. Bits 2–6 of
CB_COLOR0_INFOare the surface format; flip them and you change what the render target is. - Switch to
DB_DEPTH_CONTROLand setZFUNCto 7. That is "always" — depth testing that never rejects anything. - Press Random a few times. Most random values decode to nonsense, which is a decent intuition for why the handlers assert so aggressively on fields they do not recognise.
Tiling arithmetic
GPUs do not store images row by row. Work out where a texel actually lands, and watch what that does to cache behaviour.
- Set the layout to linear and look at the bottom line: a 2×2 bilinear fetch touches two cache lines. Switch to tiled and it touches one. That single difference is the entire reason tiling exists.
- Change bytes-per-texel from 4 to 16 and watch the offsets stretch — and the locality benefit shrink, because fewer texels now fit in a line.
- Click around the grid and watch which other texels light up as sharing your cache line.
Run a shader across a wave
A working interpreter for part of the RDNA 2 instruction set. This is the clearest way to see why converting GPU control flow to SPIR-V is the recompiler's hardest job.
- Step the if / else program to the end. Watch
EXECchange ats_and_saveexec_b64— that instruction is the branch. No jump happened; a mask was recomputed. - Notice that the then body and the else body both execute, one after the other. All 64 lanes pay for both. That is what divergence costs.
- Set lane data to all positive and run again. Zero divergence, and the else body still runs — with
EXECat zero, so it writes nothing. - Try early exit:
s_cbranch_execzis the one place a real jump happens, and only because every lane failed. - Switch to nested if and watch a second saved mask appear in the scalar registers. This is the nesting the structuriser has to recover as merge blocks.
One instruction, all four forms
What the recompiler holds at each stage: raw bits, a decoded instruction, IR, and emitted SPIR-V.
- Start with
v_add_f32— a near one-to-one mapping, and the easy case. - Now look at
buffer_load_dword. The hardware bounds-checks buffer access for free; SPIR-V does not, so the emitter has to generate a comparison and a branch. This is why some instructions cannot appear in a loop header. s_and_saveexec_b64has no SPIR-V equivalent at all — it is the control flow, and what it becomes depends on whether structurisation succeeded.- Toggle neg, abs and clamp and watch extra SPIR-V appear. RDNA 2 folds these into the encoding; SPIR-V needs them spelled out.
Build a command buffer
Append packets, press Run, and find out whether you produced a frame. A draw packet carries almost nothing — everything it needs must already be in the register file.
- Append only a draw and run it. The error names exactly which registers are missing — that is the state a draw silently depends on.
- Add the render target, both shaders and the primitive type, then draw. Now it works, but nothing appears: you never flipped.
- Press Load a valid frame to see the minimum viable sequence, then remove packets one at a time and watch where it breaks.
- Look at the encoded dwords. They come from the real
KYTY_PM4macro, so the count field is genuinelylen − 2.
Timeline ticks and resource lifetime
Why a staging buffer cannot be freed when a draw is recorded, how timeline ticks prove completion, and how the command pool grows when every reusable buffer is still in flight.
- Fill the initial four-buffer batch without completing any work. The next commit grows the pool by four instead of blocking on a per-buffer fence.
- Complete the oldest and watch its retained resources release. They could not have been freed earlier: the GPU was still reading them.
- Notice the tick only ever increases. That monotonic counter is what the timeline semaphore gives Kyty for reuse and deferred-lifetime decisions.
Pipeline cache keys
Vulkan bakes fixed-function state into an immutable pipeline object. Toggle state and watch how quickly the cache multiplies.
- Press Look up — a miss, and a pipeline is created. Press it again with nothing changed: a hit.
- Change one setting and look up again. Another miss, another pipeline. Do this a few times and watch how a handful of toggles produces a long cache list — this is why shader-heavy games stutter on first encounter with each state combination.
- Turn depth test off and note that the fields depending on it grey out and zero in the key. Kyty does the same thing: state that cannot affect the result must not affect the hash, or you get duplicate pipelines that are actually identical.
Built by reading the KytyPS5 source tree at main commit d2413fc2 (7 October 2026). The constants, bit layouts, band boundaries and packet encodings in these tools are taken from the source; the simulations around them are simplified models, and each says so where it matters — the tiling tool in particular uses a 2×2 interleave rather than AMD's real swizzle, and the wave simulator implements a small slice of the instruction set rather than all of it.
These are unofficial. Where a tool and the code disagree, the code is right. KytyPS5 is GPL-2.0-only and based on InoriRus/Kyty (MIT). Not affiliated with Sony Interactive Entertainment.