Merge pull request #6160 from fma-rc/feat/agc-fs-table-calls

feat(agc): link calls through the AGC FS table
This commit is contained in:
Adrià authored and GitHub committed 2026-10-18 19:44:10 +02:00
commit c34e9d855c
7 files changed
+204 -13

No files matched your search

@@ -13,10 +13,13 @@
#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "ShaderDiskCache.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include <atomic>
#include <cstdint>
#include <cstdio>
#include <cerrno>
#include <exception>
#include <stdexcept>
#include <system_error>
#include <thread>
#ifdef _WIN32
@@ -26,6 +29,7 @@
#include <windows.h>
#else
#include <pthread.h>
#include <sys/mman.h>
#endif
namespace AgcDriver::DriverDetail {
@@ -48,6 +52,45 @@ void PrintFailure(std::exception_ptr error) {
std::fflush(stderr);
}
constexpr std::uint64_t DriverMemoryAddress = 0xfe0000000ull;
constexpr std::size_t DriverMemoryBytes = 2u << 20u;
constexpr std::uint64_t FsTableAddress = 0xfe0040000ull;
constexpr std::uint64_t FsTableSlotBytes = 16u;
constexpr std::uint64_t FsTableSlots = 6u;
constexpr std::uint64_t FsRoutineRecords = 8u;
constexpr std::uint64_t FsRoutineRecordBytes = 16u;
constexpr std::uint64_t FsRoutineTableBytes = FsRoutineRecords * FsRoutineRecordBytes;
constexpr std::uint64_t FsRoutineTableAddress = FsTableAddress + FsTableSlots * FsTableSlotBytes;
constexpr std::uint64_t FsRoutineAddress = FsRoutineTableAddress + FsRoutineTableBytes;
constexpr std::uint32_t ScalarSetpcB64LinkS14 = 0xbe80200eu;
constexpr std::uint32_t ScalarEndProgram = 0xbf810000u;
constexpr std::uint64_t FsTableDword3 = 0x5204u;
void MapDriverMemory() {
void* mapped;
#ifdef _WIN32
mapped = reinterpret_cast<void*>(DriverMemoryAddress);
GuestArena::GuestArenaCommit_nid_postfix(mapped, DriverMemoryBytes, PAGE_READWRITE, DriverMemoryBytes);
#else
mapped = mmap(reinterpret_cast<void*>(DriverMemoryAddress), DriverMemoryBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
if (mapped == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map the AGC driver memory");
#endif
if (reinterpret_cast<std::uint64_t>(mapped) != DriverMemoryAddress) throw std::runtime_error("the AGC driver memory was not mapped at its fixed address");
auto* routineTable = reinterpret_cast<std::uint64_t*>(FsRoutineTableAddress);
for (std::uint64_t record = 0; record < FsRoutineRecords; ++record) {
routineTable[record * 2u] = FsRoutineAddress;
routineTable[record * 2u + 1u] = 0u;
}
auto* routine = reinterpret_cast<std::uint32_t*>(FsRoutineAddress);
routine[0] = ScalarSetpcB64LinkS14;
routine[1] = ScalarEndProgram;
auto* slot = reinterpret_cast<std::uint64_t*>(FsTableAddress);
slot[0] = (FsRoutineTableAddress & 0xffffffffffffull) | 16ull << 48u;
slot[1] = FsTableDword3 << 32u | FsRoutineRecords;
GuestAllocations::Mutation mutation;
mutation.Add(mapped, DriverMemoryBytes, true, true, true);
}
bool StackHoldsImport() {
#ifdef _WIN32
ULONG_PTR low = 0;
@@ -137,6 +180,8 @@ void Driver::stop() {
Driver::Driver() {
InstallAsyncLog_nid_no_patch();
static const bool driverMemory = (MapDriverMemory(), true);
static_cast<void>(driverMemory);
static_cast<void>(GuestMemory::TrackerGeneration());
ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs);
try {
@@ -652,7 +652,7 @@ std::vector<ShaderRecompiler::TableCallee> ReadTableCallees(const ShaderRecompil
constexpr std::uint64_t limitBytes = 64u << 20u;
constexpr std::size_t limitCallees = 64u;
const auto slot = call.userDataRegister - request.context.userDataBaseRegister;
const auto pointer = static_cast<std::uint64_t>(request.context.userData[slot]) | static_cast<std::uint64_t>(request.context.userData[slot + 1u]) << 32u;
const auto pointer = call.constantPointer ? *call.constantPointer : static_cast<std::uint64_t>(request.context.userData[slot]) | static_cast<std::uint64_t>(request.context.userData[slot + 1u]) << 32u;
const auto words = ReadGuestBytes(pointer + call.descriptorOffset, 16u, "call table descriptor");
ShaderRecompiler::ShaderBufferResource descriptor{};
std::memcpy(descriptor.fields.data(), words.data(), words.size());
@@ -0,0 +1,114 @@
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "SceShaders.hpp"
#include "VulkanTestDevice.hpp"
#include <array>
#include <cstdint>
#include <cstring>
#include <iostream>
#include <span>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
constexpr std::uint32_t Threads = 64;
constexpr std::uint32_t Untouched = 0xcafebabeu;
constexpr std::uint32_t Marker = 0x55u;
constexpr std::uint64_t FsTable = 0xfe0040000ull;
alignas(256) constexpr std::array<std::uint32_t, 32> Caller{
0x34040082u, 0x7e0202ffu, 0x00000055u, 0xbeea03ffu, 0xe0040000u, 0xbeeb038fu, 0xf4080235u, 0xfa000000u, 0xbf8cc07fu, 0xf4240384u,
0xfa000020u, 0xbf8cc07fu, 0xbf130e80u, 0xbf840006u, 0xf4240404u, 0xfa000028u, 0xbf8cc07fu, 0xbe8e210eu, 0xe0701000u, 0x80010102u,
0xbf810000u, 0xbf9f0000u};
alignas(256) std::array<std::uint32_t, Threads> Output{};
std::vector<std::uint32_t> Commands() {
std::vector<std::uint32_t> words;
const auto registers = [&](std::uint32_t first, std::span<const std::uint32_t> values) {
words.push_back(0xc0007600u | (static_cast<std::uint32_t>(values.size()) << 16u));
words.push_back(first);
words.insert(words.end(), values.begin(), values.end());
};
const std::array<std::uint32_t, 3> shape{Threads, 1, 1};
registers(0x207, shape);
const auto address = reinterpret_cast<std::uintptr_t>(Caller.data());
const std::array<std::uint32_t, 2> program{static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(address >> 40u)};
registers(0x20c, program);
const std::array<std::uint32_t, 1> resource{8u << 1u};
registers(0x213, resource);
const auto output = reinterpret_cast<std::uintptr_t>(Output.data());
const std::array<std::uint32_t, 8> data{0u, 0u, 0u, 0u, static_cast<std::uint32_t>(output), static_cast<std::uint32_t>((output >> 32u) & 0xffffu),
static_cast<std::uint32_t>(sizeof(Output)), 0x31016facu};
registers(0x240, data);
words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u});
return words;
}
std::string Dispatch() {
Output.fill(Untouched);
const auto words = Commands();
Packet packet{const_cast<std::uint32_t*>(words.data()), static_cast<std::uint32_t>(words.size()), 0, {}};
std::string failure;
try {
AgcDriver::Submit(&packet, 0);
AgcDriverWaitIdle_nid_postfix();
} catch (const std::exception& error) {
failure = error.what();
}
AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(Output.data()), sizeof(Output));
return failure;
}
void CheckTable() {
AgcDriverWaitIdle_nid_postfix();
Require(AgcDriver::GuestMemory::Accessible(reinterpret_cast<const void*>(FsTable), 0x60u), "the FS table at 0xfe0040000 is not mapped");
std::array<std::uint64_t, 12> slots{};
std::memcpy(slots.data(), reinterpret_cast<const void*>(FsTable), sizeof(slots));
const auto records = slots[0] & 0xffffffffffffull;
Require(slots[0] >> 48u == 16u, "FS table slot 0 does not have a stride of 16");
Require(slots[1] == 0x0000520400000008ull, "FS table slot 0 does not describe eight records with dword3 0x5204");
for (std::size_t slot = 2; slot < slots.size(); ++slot) Require(slots[slot] == 0u, "an FS table slot other than the first is not zero");
Require(records % 16u == 0u && AgcDriver::GuestMemory::Accessible(reinterpret_cast<const void*>(records), 128u), "the FS table records are not readable");
std::array<std::uint64_t, 16> table{};
std::memcpy(table.data(), reinterpret_cast<const void*>(records), sizeof(table));
for (std::size_t record = 0; record < 8u; ++record) {
Require(table[record * 2u] != 0u && table[record * 2u] == table[2u] && table[record * 2u + 1u] == 0u, "an FS table record does not hold the routine and a zero");
}
std::array<std::uint32_t, 2> routine{};
Require(AgcDriver::GuestMemory::Accessible(reinterpret_cast<const void*>(table[2]), sizeof(routine)), "the FS routine is not readable");
std::memcpy(routine.data(), reinterpret_cast<const void*>(table[2]), sizeof(routine));
Require(routine[0] == 0xbe80200eu, "the FS routine does not start with s_setpc_b64 s[14:15]");
}
}
int main() {
try {
{
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
}
CheckTable();
const auto failure = Dispatch();
Require(failure.find("not statically resolvable") == std::string::npos, "the call through the FS table did not link: " + failure);
Require(failure.empty(), "the dispatch failed: " + failure);
for (std::uint32_t lane = 0; lane < Threads; ++lane) {
Require(Output[lane] == Marker, "lane " + std::to_string(lane) + " is " + std::to_string(Output[lane]) + " after the call through the FS table, expected " + std::to_string(Marker));
}
AgcDriverShutdown_nid_postfix();
std::puts("compute FS table call tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
try {
AgcDriverShutdown_nid_postfix();
} catch (const std::exception&) {
}
return 1;
}
}
+18
View File
@@ -198,7 +198,25 @@ static IrProgram TranslateCode(std::span<const std::uint32_t> code, const Swappc
return InstructionTranslator{}.Translate(program, cfg, options);
}
static void CheckConstantTableCall() {
const std::vector<std::uint32_t> caller{0xbeea03ffu, 0xe0040000u, 0xbeeb038fu, 0xf4080135u, 0xfa000000u, 0xbf8cc07fu, 0xf4240382u, 0xfa000020u, 0xbf8cc07fu, 0xbf130e80u,
0xbf840004u, 0xf4240402u, 0xfa000028u, 0xbf8cc07fu, 0xbe8e210eu, 0xbf810000u};
const SwappcInfo swappc{false, 0u, 16u};
const auto calls = FindTableCalls(RdnaInstructionDecoder{}.Decode(caller), swappc);
Require(calls.size() == 1u);
Require(calls[0].constantPointer == std::optional<std::uint64_t>(0xfe0040000ull) && calls[0].descriptorOffset == 0u && calls[0].entryOffset == 0x20u && calls[0].entryStride == 0u);
Require(calls[0].linkRegister == 14u && calls[0].targetRegister == 14u);
auto changedHigh = caller;
changedHigh[2] = 0xbeeb0380u;
const auto zero = FindTableCalls(RdnaInstructionDecoder{}.Decode(changedHigh), swappc);
Require(zero.size() == 1u && zero[0].constantPointer == std::optional<std::uint64_t>(0xe0040000ull));
auto computed = caller;
computed[2] = 0xbeeb0302u;
Require(FindTableCalls(RdnaInstructionDecoder{}.Decode(computed), swappc).empty());
}
static void CheckTableCalls() {
CheckConstantTableCall();
const SwappcInfo swappc{false, 0u, 16u};
const auto caller = TableCaller();
const auto program = RdnaInstructionDecoder{}.Decode(caller);
@@ -51,6 +51,7 @@ struct TableCall {
std::uint32_t descriptorOffset = 0;
std::uint32_t entryOffset = 0;
std::uint32_t entryStride = 0;
std::optional<std::uint64_t> constantPointer;
};
struct TableCallee {
@@ -294,10 +294,11 @@ bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index, std::
return covers(instruction.destination) || covers(instruction.destination2);
}
bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list<std::uint32_t> indices, std::uint32_t& writer, std::uint32_t waveSize = 0u) {
bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list<std::uint32_t> indices, std::uint32_t& writer, std::uint32_t waveSize = 0u, std::uint32_t skipBranchesPast = 0u) {
for (auto position = end; position-- > 0u;) {
const auto& instruction = program.instructions[position];
if (IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SSetpcB64 || instruction.op == RdnaOpcode::SEndpgm) return false;
const bool skipped = skipBranchesPast != 0u && IsDirectBranchOpcode(instruction.op) && instruction.op != RdnaOpcode::SBranch && instruction.branchTarget > skipBranchesPast;
if ((IsDirectBranchOpcode(instruction.op) && !skipped) || instruction.op == RdnaOpcode::SSetpcB64 || instruction.op == RdnaOpcode::SEndpgm) return false;
if (std::ranges::any_of(indices, [&](std::uint32_t index) { return writesScalar(instruction, index, waveSize); })) {
writer = position;
return true;
@@ -1286,10 +1287,10 @@ bool isScalarBufferLoad(RdnaOpcode op) {
}
}
bool loadedDword(const RdnaProgram& program, std::uint32_t reg, std::uint32_t before, std::uint32_t waveSize, std::uint32_t& load, std::uint32_t& dword) {
bool loadedDword(const RdnaProgram& program, std::uint32_t reg, std::uint32_t before, std::uint32_t waveSize, std::uint32_t skipBranchesPast, std::uint32_t& load, std::uint32_t& dword) {
for (std::uint32_t copies = 0; copies < 4u; ++copies) {
std::uint32_t writer = 0;
if (!findLastWriter(program, before, {reg}, writer, waveSize)) return false;
if (!findLastWriter(program, before, {reg}, writer, waveSize, skipBranchesPast)) return false;
const auto& instruction = program.instructions[writer];
const auto destination = scalarIndex(instruction.destination);
const auto source = scalarIndex(instruction.source0);
@@ -1445,28 +1446,39 @@ std::vector<TableCall> FindTableCalls(const RdnaProgram& program, const SwappcIn
const auto target = scalarIndex(instruction.source0);
if (link == NoScalarRegister || link % 2u != 0u || link > 104u || target == NoScalarRegister || target % 2u != 0u || target > 104u) continue;
std::uint32_t lowLoad = 0, lowDword = 0, highLoad = 0, highDword = 0;
if (!loadedDword(program, target, index, swappc.waveSize, lowLoad, lowDword) || !loadedDword(program, target + 1u, index, swappc.waveSize, highLoad, highDword)) continue;
const auto reader = instruction.programCounter;
if (!loadedDword(program, target, index, swappc.waveSize, reader, lowLoad, lowDword) || !loadedDword(program, target + 1u, index, swappc.waveSize, reader, highLoad, highDword)) continue;
if (lowLoad != highLoad || highDword != lowDword + 1u || !sameBlock(lowLoad, index)) continue;
const auto& load = program.instructions[lowLoad];
const auto descriptor = scalarIndex(load.source0);
if (load.source0.kind != RdnaOperandKind::ScalarRegister || descriptor % 4u != 0u || load.memoryOffset >= 0x100000u || (load.memoryOffset & 3u) != 0u) continue;
std::uint32_t descriptorLoad = 0;
if (!findLastWriter(program, lowLoad, {descriptor, descriptor + 1u, descriptor + 2u, descriptor + 3u}, descriptorLoad, swappc.waveSize) || !sameBlock(descriptorLoad, index)) continue;
if (!findLastWriter(program, lowLoad, {descriptor, descriptor + 1u, descriptor + 2u, descriptor + 3u}, descriptorLoad, swappc.waveSize, reader) || !sameBlock(descriptorLoad, index)) continue;
const auto& table = program.instructions[descriptorLoad];
const auto pointer = scalarIndex(table.source0);
if (table.op != RdnaOpcode::SLoadDwordx4 || !isScalar(table.destination, descriptor) || table.source0.kind != RdnaOperandKind::ScalarRegister || table.source1.kind != RdnaOperandKind::Null ||
table.memoryOffset >= 0x100000u || (table.memoryOffset & 3u) != 0u || !userData(pointer) || !userData(pointer + 1u) ||
!unchangedUntil(program, pointer, descriptorLoad, swappc.waveSize) || !unchangedUntil(program, pointer + 1u, descriptorLoad, swappc.waveSize)) continue;
if (table.op != RdnaOpcode::SLoadDwordx4 || !isScalar(table.destination, descriptor) || pointer == NoScalarRegister || pointer % 2u != 0u || table.source1.kind != RdnaOperandKind::Null ||
table.memoryOffset >= 0x100000u || (table.memoryOffset & 3u) != 0u) continue;
std::optional<std::uint64_t> constantPointer;
if (!(userData(pointer) && userData(pointer + 1u) && unchangedUntil(program, pointer, descriptorLoad, swappc.waveSize) && unchangedUntil(program, pointer + 1u, descriptorLoad, swappc.waveSize))) {
std::uint32_t lowWriter = 0, highWriter = 0, low = 0, high = 0;
if (!findLastWriter(program, descriptorLoad, {pointer}, lowWriter, swappc.waveSize, reader) || !findLastWriter(program, descriptorLoad, {pointer + 1u}, highWriter, swappc.waveSize, reader) ||
!sameBlock(std::min(lowWriter, highWriter), descriptorLoad)) continue;
const auto& lowMove = program.instructions[lowWriter];
const auto& highMove = program.instructions[highWriter];
if (lowMove.op != RdnaOpcode::SMovB32 || highMove.op != RdnaOpcode::SMovB32 || !isScalar(lowMove.destination, pointer) || !isScalar(highMove.destination, pointer + 1u) ||
!isImmediate(lowMove.source0, low) || !isImmediate(highMove.source0, high)) continue;
constantPointer = static_cast<std::uint64_t>(high) << 32u | low;
}
std::uint32_t stride = 0;
if (load.source1.kind != RdnaOperandKind::Null) {
const auto offset = scalarIndex(load.source1);
std::uint32_t shift = 0, shiftIndex = 0;
if (offset == NoScalarRegister || !findLastWriter(program, lowLoad, {offset}, shiftIndex, swappc.waveSize) || !sameBlock(shiftIndex, index)) continue;
if (offset == NoScalarRegister || !findLastWriter(program, lowLoad, {offset}, shiftIndex, swappc.waveSize, reader) || !sameBlock(shiftIndex, index)) continue;
const auto& shifted = program.instructions[shiftIndex];
if (shifted.op != RdnaOpcode::SLshlB32 || !isScalar(shifted.destination, offset) || !isImmediate(shifted.source1, shift) || shift < 2u || shift > 24u) continue;
stride = 1u << shift;
}
calls.push_back({instruction.programCounter, link, target, pointer, table.memoryOffset, load.memoryOffset + lowDword * 4u, stride});
calls.push_back({instruction.programCounter, link, target, constantPointer ? 0u : pointer, table.memoryOffset, load.memoryOffset + lowDword * 4u, stride, constantPointer});
}
return calls;
}
+2 -1
View File
@@ -167,7 +167,8 @@
- An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) whose record range overlaps a buffer or storage image written by its own dispatch faults (`table written by its dispatch`) when any entry is selected, even if the write misses the selected record. A selected column's range is its record and the bytes of the record before the descriptor.
- An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) T# whose surface has no mapped byte faults when selected instead of becoming a zero texture as a direct T# does; what the console returns for it is not measured.
- [User-data callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped from the recompiled code; in the shaders seen they are metadata or constants the caller reads relative to its program counter, and those reads go to the captured guest code, which linking does not change. A caller that jumps through a table among those words, which the control flow analysis reads from the recompiled code, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code.
- [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block, calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4` without SOFFSET from a user-data pointer and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs.
- [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block (a conditional branch forward past the call between them is crossed), calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4` without SOFFSET from a user-data pointer, or from a pointer pair that two `s_mov_b32` of a literal or inline constant set, and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs.
- [AGC FS table](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp) (libSceAgcDriver) - the driver maps 2 MiB at 0xF_E0000000 when it is created and fills the table that libSceAgc keeps at 0xF_E0040000 (six 16-byte V# slots, slot 0 describing eight 16-byte records) the way PS5 12.00 libSceAgcDriver and libSceAgc do, but every record points at one AnyPS5 routine that only returns (`s_setpc_b64` on the link pair). Sony's routines, which order work before a title's `ds_ordered_count`, are not modeled, and the other five slots stay zero.
- [DB_RENDER_CONTROL resummarize draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing. A pure resummarize with pass-through D16/D32 depth, no depth writes or bounds test, no stencil plane, no color writes and no running pixel stage also draws nothing; other resummarize, copy or decompress draws throw.
- [Comparison-sampled R32 float and R16 unorm textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained.
- [Null vertex buffer V#](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware.