Our own translator instead of shadPS4: structure and order of work (2026-10-10)¶
Goal: move away from emulation and from shadPS4 code. We want our own PS4 command translator and
our own graphics renderer that scale to other games (everything specific to one game lives in its
profile, gpu/games/) and behave the same on AMD and NVIDIA. After that comes shadnet (online play).
Current state¶
Borrowed from shadPS4 (gpu/VENDOR.txt, commit b4e7ae7 from 2026-09-24): about 116k lines.
| Part | Lines | What it does | Our work in it |
|---|---|---|---|
video_core/amdgpu |
8.3k | PM4 parsing (command processor liverpool.cpp), registers, tiling |
GPU-side labels, occlusion, predication, PM4 self-test |
video_core/renderer_vulkan |
28.6k | Vulkan renderer: pipelines, rasterizer, scheduler, presenter | draw pipeline (stages A/B), recording threads, upscalers, background compilation |
video_core/buffer_cache |
7.4k | game memory for the GPU | almost all rewritten (memory module, VRAM copies, traps) |
video_core/texture_cache |
4.8k | textures by address, detiling | time-based collector, uploads |
shader_recompiler |
38.6k | GCN → IR → SPIR-V | paged buffers (BDA), fixes |
core, common |
21.8k | VideoOut, GnmDriver, event queues, utilities | vblank, labels |
Our own code, outside shadPS4: gpu/layer/ (memory module BbLayer::GpuMemory), gpu/games/ (game
profiles), gpu/shim/ (overlay, settings, hooks), the runtime in src/ (kernel, libc, files, saves).
Target structure¶
Modules with narrow interfaces. None of them knows addresses in the game's code (that belongs to the profile), and none does the GPU's work on the CPU (that is emulation):
- memory: game memory in place, VRAM copies as a cache, shared tracking of page protection
(write traps for copies and textures). Exists:
layer/+ the module inbuffer_cache_layer.cpp. - cp: command processor. Parses PM4 into the translator's command stream (state, draws,
dispatches, synchronization) in the order the GPU executes it. Replaces
liverpool.cpp. - state: context registers → pipeline keys and Vulkan dynamic state. Replaces
liverpool_to_vkand the key building invk_pipeline_cache. - resources: V#/T#/S# descriptors → buffers, images, samplers (caches, detiling). Replaces
buffer_cache,texture_cache(the buffer part is already almost ours). - render: Vulkan. Command recording per thread, barriers, submission, presentation. Replaces
renderer_vulkan(the scheduler and stages A/B are already ours). - shaders: GCN → SPIR-V. The largest part; it is replaced last, stage by stage (decoder, IR, passes, code generation).
- platform: VideoOut, GnmDriver, kernel events (
core/libraries), next to the runtime. - games: game profiles (exists).
Order¶
One module is replaced at a time. First an interface in our code with the shadPS4 code behind it
as is, then our own implementation behind the same interface. After each step: the route (a route
benchmark script (not in this repository), frames and snapshots), the PM4 self-test
(BB_PM4_SELFTEST=1), unit tests, the game on AMD and (from tester logs) on NVIDIA.
| Step | What | Why in this order |
|---|---|---|
| 1 | memory: shared tracking of page protection; write traps under textures by default; model 0.3 and the sparse arena are removed | removes the rest of memory emulation and the dependency on game hooks |
| 2 | cp: the translator's command stream; MEM_SEMAPHORE, GPU time, compute queues done by the GPU |
the last emulation left is in the command processor |
| 3 | state + resources | they build on 1 and 2; the buffer part is already ours |
| 4 | render | the scheduler and stages are already ours; the rasterizer and pipelines remain |
| 5 | shaders | the largest and riskiest; by then everything else is already ours |
| 6 | shadnet | networking, once the graphics are our own |
Steps 1 and 2 are the plan to move away from emulation
(EMULATION_REMOVAL_PLAN.ru.md,
section dated 2026-10-10).
Progress¶
2026-10-10: our own PM4 decoder (step 2, start)¶
gpu/cp/: our own code, without shadPS4. Packet format and opcodes (cp_pm4.h), register file and decoder (cp_decoder.{h,cpp}). The decoder walks the buffer in stream order, applies register packets (SET_*_REG,CLEAR_STATE, registers written by draw packets), descends into nested buffers and hands each packet to a "sink" (BbCp::Sink) with the state already applied: draws and dispatches as typed calls, the rest as packets. The sink decidesCOND_EXEC. The decoder knows nothing about Vulkan, the game or where execution happens.- Unit test
tests/test_cp_decoder.cpp(targetcp-decoder-test): registers, partial writes, draws, dispatches, nested buffers,COND_EXEC,CLEAR_STATE, corrupt buffers. - Register addresses are checked against the shadPS4 layout at build time (
static_assert). This found a bug: on CIK,CP_STRMOUT_CNTLis in uconfig (0xC03F), not in config. - Shadow check in the game (
BB_CP_SHADOW=1): the new decoder parses every top-level buffer from the same state, and the register files are compared at the end of the buffer. On the route, all buffers matched (~800k buffers, ~200 million packets, 0 mismatches, 0 parse errors). Bloodborne does not use nested buffers (they are covered by the unit test). - Next: the decoder as a cursor (packet by packet, so the command processor loop can yield to
other queues while waiting), the register file as shared state storage (its layout matches
shadPS4
Regsword for word), then moving packet handling fromliverpool.cppinto the translator's sink one packet type at a time, each time with the shadow check and the route.
2026-10-10: the new decoder is the real parsing path¶
- The decoder became a cursor (
BbCp::Cursor: packet by packet, nested buffers at the caller's choice). The register file can live in the owner's storage (for the command processor, itsregs, with the same word layout). A write observer (RegisterObserver, one call per packet) provides what draw preparation needs: the written blocks and the packet checksum. ProcessGraphicstakes packets and register writes from the cursor (BB_CP_DECODER=0selects the old walk). What each packet does is still the old code. PM4 self-test: 11 of 11. The share of prepared draws is the same (747k used, 19k not, versus 755k/24k), so the checksums match. The image is the same.