83 lines
6.1 KiB
Markdown
83 lines
6.1 KiB
Markdown
# Architecture
|
||
|
||
How a PS5 executable becomes a native Linux or Windows program. Nothing is emulated: the converted executable runs as a normal process and calls native implementations of the system libraries.
|
||
|
||
## Overview
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph input["PS5 game"]
|
||
elf["input.elf"]
|
||
mods["sce_module/*"]
|
||
end
|
||
|
||
subgraph relinker["relinker (core/relinker)"]
|
||
direction TB
|
||
intel["--to-intel:<br/>lower AMD-only instructions<br/>(codegen)"]
|
||
pipeline["RelinkerPipeline:<br/>read imports by NID,<br/>check syscalls, filter unused NIDs,<br/>build SysV dynamic section"]
|
||
guest["GuestModuleBuilder:<br/>convert bundled modules"]
|
||
patcher["LinuxElfPatcher / WindowsPePatcher"]
|
||
intel -.-> pipeline --> guest --> patcher
|
||
end
|
||
|
||
subgraph build["build (core/libs)"]
|
||
direction TB
|
||
prx["core/libs/prx/*<br/>shared libraries"]
|
||
nid["nid_patcher:<br/>rename exports to their NIDs"]
|
||
prx --> nid
|
||
end
|
||
|
||
subgraph out["Native program"]
|
||
app["app.elf / app.exe"]
|
||
app0["app0/sce_module/*"]
|
||
libs["libs/*.prx"]
|
||
end
|
||
|
||
elf --> intel
|
||
mods --> guest
|
||
patcher --> app
|
||
guest --> app0
|
||
nid --> libs
|
||
app -- "OS loader binds imports by NID" --> libs
|
||
app0 -- "OS loader binds imports by NID" --> libs
|
||
```
|
||
|
||
- [`core/relinker/main.cpp`](../../core/relinker/main.cpp) runs the steps in this order. `--to-intel` is optional, see [USAGE.md](../user/USAGE.md).
|
||
- Each library in [`core/libs/prx`](../../core/libs/prx) builds as a shared library. After the build, `nid_patcher` ([`core/libs/nid`](../../core/libs/nid)) renames every export to its NID, computed from the function name. `APS5_EXPORT("<nid>", func)` sets the NID directly when the name is unknown.
|
||
|
||
## Graphics
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
game["Game:<br/>command buffers"] --> submit["libSceAgcDriver/Submit<br/>DCB / ACB"]
|
||
submit --> pm4["Execution/Pm4:<br/>state, draws,<br/>dispatches"]
|
||
pm4 -- "shader + state" --> cache{"Compiled variant<br/>in memory or<br/>on disk?"}
|
||
cache -- yes --> vk
|
||
cache -- no --> dec
|
||
|
||
subgraph recompiler["core/shader/recompiler"]
|
||
dec["RdnaDecoder"] --> cf["ControlFlow:<br/>graph + structurize"]
|
||
cf --> tr["Translation:<br/>RDNA to IR"]
|
||
tr --> opt["Optimization:<br/>SSA, resources,<br/>bindings"]
|
||
opt --> spv["SpirvBackend:<br/>emit SPIR-V"]
|
||
end
|
||
|
||
spv --> vk["libSceAgcDriver/Graphics:<br/>Vulkan pipeline"]
|
||
```
|
||
|
||
- [`Recompiler.cpp`](../../core/shader/recompiler/Recompiler.cpp) runs the stages in this order. With `ANYPS5_ENABLE_SPIRV_TOOLS`, the SPIR-V is also validated and optimized with SPIRV-Tools.
|
||
- `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them.
|
||
- A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory.
|
||
- `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks).
|
||
- The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead.
|
||
|
||
### Shader MODE register access
|
||
|
||
`GuestContext::floatMode` carries the initial shader mode through `Recompile` and `TranslateOptions` into each block's `TranslationContext`. `s_getreg_b32` reads confined to MODE bits 0–3 return the selected initial rounding bits in the destination's low bits. Bits 0–1 select f32 rounding; bits 2–3 select f16/f64 rounding. Missing mode metadata retains the legacy round-to-nearest-even assumption. Reading other MODE fields or hardware registers throws.
|
||
|
||
An immediate MODE write confined to the rounding fields is accepted only when its selected replacement bits equal the initial state. Source bits above the selected width are ignored. `s_round_mode` similarly compares its low four immediate bits with the initial rounding fields. A state-changing write or an SGPR-sourced MODE write throws with its instruction and program counter. Runtime transitions require control-flow-aware mode tracking and matching arithmetic lowering. Writes to other hardware registers retain their existing behavior and remain technical debt.
|
||
|
||
The register layout and instruction contract follow [AMD RDNA2 ISA sections 5.8 and 6.4, Table 24](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna2-shader-instruction-set-architecture.pdf), cross-checked against LLVM 20.1.8's [hardware-register encoding](https://github.com/llvm/llvm-project/blob/87f0227cb60147a26a1eeb4fb06e3b505e9c7261/llvm/lib/Target/AMDGPU/Utils/AMDGPUBaseInfo.h) and [MODE state analysis](https://github.com/llvm/llvm-project/blob/87f0227cb60147a26a1eeb4fb06e3b505e9c7261/llvm/lib/Target/AMDGPU/SIModeRegister.cpp). This contract does not establish arithmetic support for every initial mode or console-specific behavior.
|
||
|
||
`agc_shader_mode_register` decodes instruction words, builds the control-flow graph and checks production translation without a Vulkan device. It covers rounding subfield reads, masked constant writes, branches, unsupported runtime changes and diagnostics, plus mode propagation through full recompilation. The requirements remain active in Release; CTest bounds execution to 20 seconds.
|