fix(shader): preserve ancillary proofs across relative VGPR accesses

This commit is contained in:
eljackjoestar-cmd 2026-10-12 03:52:35 +04:00
parent 9aa5fd2900
commit b3587f9528
5 changed files with 191 additions and 6 deletions

View File

@ -0,0 +1,148 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <array>
#include <cstdint>
#include <iostream>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Extent = 64u;
alignas(256) std::array<std::byte, Extent * Extent * 4u> pixels{};
alignas(256) constexpr std::array<std::array<float, 4>, 6> positions{{
{0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 0.0f, 1.0f},
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
}};
constexpr std::array<std::uint32_t, 6> vertexCode{
0xe0382000u, 0x80000005u, 0xbf8c3f70u, 0xf80008cfu, 0x03020100u, 0xbf810000u
};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
}
void Draw(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> fragmentCode, std::uint32_t offset, std::uint32_t waveSize, std::byte expected, bool reject = false, bool implicitInputs = false) {
pixels.fill(std::byte{0x40});
const auto target = device.Target();
const auto userData = BufferDescriptor(positions.data(), 16u, positions.size());
const auto code = std::span<const std::uint32_t>(vertexCode);
ShaderRecompiler::RecompileRequest vertex{
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
{32u, 0u, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, {}},
target, {0u, 0u, 0u, 64u}
};
vertex.useCache = false;
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
const auto pushBytes = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
ShaderRecompiler::ShaderPixelStageInfo pixel{};
pixel.wave32 = waveSize == 32u;
pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary);
pixel.ancillary = true;
if (implicitInputs) {
pixel.inputAddr |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionZ) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionW);
pixel.posX = pixel.posY = pixel.posZ = pixel.posW = true;
}
pixel.targetOutputMode[0] = 9u;
pixel.targetExportMapping.fill(0xe4u);
const std::array<std::uint32_t, 1> pixelData{offset};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(fragmentCode.data()), fragmentCode, 0, {}},
{waveSize, 0u, pixelData, std::nullopt, pixel, std::nullopt, {}}, target, PixelPushLayout(pushBytes, target)
};
fragment.useCache = false;
const auto pixelResult = [&] {
try {
auto result = ShaderRecompiler::Recompile(fragment);
Require(!reject, "an unsupported ancillary word was accepted");
return result;
} catch (const std::exception& error) {
if (!reject || std::string(error.what()).find("packed pixel ancillary input has an unsupported live use") == std::string::npos) throw;
return ShaderRecompiler::RecompileResult{};
}
}();
if (reject) return;
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &vertexResult, 0u}, {ShaderStage::Fragment, &pixelResult, PixelPushOffset(pushBytes, target)}
}};
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt};
state.color = {reinterpret_cast<std::uintptr_t>(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u};
state.colors = {state.color};
state.hasColorTarget = true;
state.renderExtent = {Extent, Extent};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0.0f, static_cast<float>(Extent), static_cast<float>(Extent), -static_cast<float>(Extent), 0.0f, 1.0f};
state.scissor = {{0, 0}, {Extent, Extent}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.blend.colorWriteMask = 15u;
state.blends = {state.blend};
AgcDriver::Pm4::DrawParameters draw{0u, 3u, 0u, 1u, 0u, false};
draw.firstVertex = 3u;
draw.firstInstance = 2u;
device.Draw(state, draw, shaders);
device.WaitIdle();
for (const auto value : pixels) Require(value == expected, "relative ancillary draw produced an incorrect pixel: " + std::to_string(std::to_integer<unsigned>(value)));
}
} // namespace
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
for (const auto waveSize : {32u, 64u}) {
const std::array<std::uint32_t, 8> read{
0xbefc0300u, 0x7e028700u, 0xd5480001u, 0x02111101u,
0x7e020d01u, 0x060202f2u, 0xf800180fu, 0x01010101u
};
auto code = std::vector<std::uint32_t>(read.begin(), read.end());
code.push_back(0xbf810000u);
std::cerr << "wave " << waveSize << " packed read\n";
Draw(*device, code, 0u, waveSize, std::byte{255});
Draw(*device, code, 4u, waveSize, std::byte{255}, false, true);
const std::array<std::uint32_t, 8> prefix{
0xd5480000u, 0x02111100u, 0x7e000d00u, 0x7e020280u,
0x7e0402f2u, 0xbefc0300u, 0x7e008502u, 0x7e008702u
};
code.assign(prefix.begin(), prefix.end());
code.insert(code.end(), {0xf800180fu, 0x01010101u, 0xbf810000u});
std::cerr << "in-range\n";
Draw(*device, code, 1u, waveSize, std::byte{255});
std::cerr << "out-of-range\n";
Draw(*device, code, 8u, waveSize, std::byte{0});
code[6] = 0x7e02d100u;
code.erase(code.begin() + 7);
std::cerr << "in-range\n";
Draw(*device, code, 0x00010001u, waveSize, std::byte{255});
std::cerr << "out-of-range\n";
Draw(*device, code, 0x00010008u, waveSize, std::byte{0});
code.assign(prefix.begin(), prefix.begin() + 6);
code.insert(code.end(), {0xbefe0480u, 0x7e008502u, 0xbefe04c1u, 0xf800180fu, 0x01010101u, 0xbf810000u});
std::cerr << "inactive write\n";
Draw(*device, code, 1u, waveSize, std::byte{0});
for (const auto modifier : {0x0000e402u, 0xff00e402u}) {
code.assign(prefix.begin(), prefix.begin() + 6);
code.insert(code.end(), {0x7e0084fau, modifier, 0xf800180fu, 0x01010101u, 0xbf810000u});
std::cerr << "DPP write " << modifier << '\n';
Draw(*device, code, 1u, waveSize, modifier == 0xff00e402u ? std::byte{255} : std::byte{0});
Draw(*device, code, 8u, waveSize, std::byte{0});
}
const std::array<std::uint32_t, 5> unsupported{0xbefc0300u, 0x7e028700u, 0xf800180fu, 0x01010101u, 0xbf810000u};
Draw(*device, unsupported, 0u, waveSize, std::byte{0}, true);
Draw(*device, unsupported, 4u, waveSize, std::byte{0}, true, true);
}
std::cout << "relative ancillary GPU readback tests passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -223,6 +223,9 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat
}
if (maskedSelectEliminator.Eliminate(program).removedSelects != 0u) {
constantFolder.Fold(program);
ResolveControlFlowIdentities(program);
deadCodeEliminator.RemoveIdentities(program);
deadCodeEliminator.Eliminate(program);
}
};

View File

@ -331,8 +331,12 @@ struct RelativeWindowAccess {
IrValue* inRange = nullptr;
};
RelativeWindowAccess relativeWindowAccess(IrBuilder& ir, const IrProgram& program, std::uint32_t reg, IrValue& offset, const char* name) {
RelativeWindowAccess relativeWindowAccess(IrBuilder& ir, const IrProgram& program, std::uint32_t reg, IrValue& offset, const char* name, std::uint32_t vectorLimit) {
const auto& metadata = program.Metadata();
if (metadata.vectorWindowSize == 0u) {
if (reg >= vectorLimit) throw std::runtime_error(std::string(name) + " addresses a register outside the relative-addressing span");
return {nullptr, &ir.ULessThan(offset, ir.Constant(vectorLimit - reg))};
}
if (metadata.vectorWindowSize == 0u || reg < metadata.vectorWindowBase) throw std::runtime_error(std::string(name) + " addresses a register outside the relative-addressing window");
const auto first = reg - metadata.vectorWindowBase;
IrValue& index = ir.Emit(IrOpcode::UMin32, IrType::U32, {&ir.IAdd(ir.Constant(first), offset), &ir.Constant(metadata.vectorWindowSize - 1u)});
@ -343,7 +347,15 @@ IrU32 TranslationContext::readRelativeVector(const RdnaOperand& base, IrValue& o
if (base.kind != RdnaOperandKind::VectorRegister) {
throw std::runtime_error(std::string(name) + " source is not a vector register");
}
const auto access = relativeWindowAccess(ir, program, base.reg, offset, name);
const auto access = relativeWindowAccess(ir, program, base.reg, offset, name, currentVectorLimit);
if (access.index == nullptr) {
IrU32 result(ir.GetVectorReg(static_cast<VectorReg>(0)));
for (std::uint32_t reg = base.reg; reg < currentVectorLimit; ++reg) {
IrValue& hit = ir.IEqual(offset, ir.Constant(reg - base.reg));
result = IrU32(ir.Select(hit, ir.GetVectorReg(static_cast<VectorReg>(reg)), result.Value()));
}
return applyBitSourceModifiers(base, result);
}
IrValue& loaded = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {access.index});
return applyBitSourceModifiers(base, IrU32(ir.Select(*access.inRange, loaded, ir.GetVectorReg(static_cast<VectorReg>(0)))));
}
@ -353,7 +365,21 @@ void TranslationContext::writeRelativeVector(const RdnaOperand& base, IrValue& o
if (destination.kind != RdnaOperandKind::VectorRegister) {
throw std::runtime_error(std::string(name) + " destination is not a vector register");
}
const auto access = relativeWindowAccess(ir, program, destination.reg, offset, name);
const auto access = relativeWindowAccess(ir, program, destination.reg, offset, name, currentVectorLimit);
if (access.index == nullptr) {
for (std::uint32_t reg = destination.reg; reg < currentVectorLimit; ++reg) {
IrValue& previous = ir.GetVectorReg(static_cast<VectorReg>(reg));
IrValue* merged = nullptr;
if (destination.dpp) {
const DppMoveFlags flags{destination.dpp8 ? DppMoveFlags::Lanes8 | destination.dppCtrl : destination.dppCtrl, static_cast<std::uint8_t>(destination.dppRowMask), static_cast<std::uint8_t>(destination.dppBankMask), destination.dppFetchInactive, destination.dppBoundCtrl};
merged = &ir.Emit(IrOpcode::DppUpdateU32, IrType::U32, {&value.Value(), &previous, &ir.GetExec()}, flags);
} else merged = &ir.Select(ir.GetExec(), value.Value(), previous);
IrValue* written = &ir.IEqual(offset, ir.Constant(reg - destination.reg));
if (enable != nullptr) written = &ir.LogicalAnd(*written, *enable);
ir.SetVectorReg(static_cast<VectorReg>(reg), ir.Select(*written, *merged, previous));
}
return;
}
IrValue& previous = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {access.index});
IrValue* merged = nullptr;
if (destination.dpp) {
@ -387,7 +413,7 @@ void TranslationContext::vMovrelsdB32(const RdnaInstruction& inst, bool split, b
return;
}
const IrU32 previous = readRelativeVector(plainOperand(inst.destination), destinationOffset, name);
IrValue& bothInRange = ir.LogicalAnd(*relativeWindowAccess(ir, program, sourceAt(inst, 0u).reg, sourceOffset, name).inRange, *relativeWindowAccess(ir, program, inst.destination.reg, destinationOffset, name).inRange);
IrValue& bothInRange = ir.LogicalAnd(*relativeWindowAccess(ir, program, sourceAt(inst, 0u).reg, sourceOffset, name, currentVectorLimit).inRange, *relativeWindowAccess(ir, program, inst.destination.reg, destinationOffset, name, currentVectorLimit).inRange);
writeRelativeVector(inst.destination, destinationOffset, value, name, &bothInRange);
writeRelativeVector(sourceAt(inst, 0u), sourceOffset, previous, name, &bothInRange);
}

View File

@ -435,7 +435,8 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con
}
}
}
if (windowBase < vectorLimit) {
const bool ancillaryWindow = options.stage == ShaderStageKind::Pixel && options.inputInfo.pixel != nullptr && options.inputInfo.pixel->psAncillary && options.inputInfo.pixel->psInputVgpr[static_cast<std::uint32_t>(PixelInput::Ancillary)] >= windowBase;
if (windowBase < vectorLimit && !ancillaryWindow) {
program.Metadata().vectorWindowBase = windowBase;
program.Metadata().vectorWindowSize = vectorLimit - windowBase;
}
@ -501,6 +502,13 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con
if (program.Metadata().vectorWindowSize != 0u) {
vectorLimit = program.Metadata().vectorWindowBase + program.Metadata().vectorWindowSize;
}
if (ancillaryWindow) {
for (const IrValue* instruction : blocks.front()->Instructions()) {
if (instruction->Opcode() == IrOpcode::SetVectorRegister) {
vectorLimit = std::max(vectorLimit, instruction->Argument(0)->Register().index + 1u);
}
}
}
for (const auto& cfgBlock : cfg.blocks) {
const auto typedIndex = blockIndices.at(cfgBlock.id);

View File

@ -663,7 +663,7 @@ Throughout the project, every function at every stage either **does exactly what
- [LINE_STIPPLE_TEX](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it
- [Draw snapshot buffer descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct.
- [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented.
- [Relative vector addressing](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in a zero-initialised function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window; the host compiler decides whether that array stays in registers or spills to scratch, which is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this.
- [Relative vector addressing](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in a zero-initialised function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window; the host compiler decides whether that array stays in registers or spills to scratch, which is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. Pixel programs whose relative span contains the packed ancillary input instead keep that span in SSA and implement indexed operations with compare/select chains. This keeps ancillary definitions visible to field lowering and masked-write proofs; storing the unmodelled whole word in a private array would require composing unknown bits. A fold after masked-select elimination lowers fields whose consumers become visible only after redundant copies are removed. Whole-word uses still throw. Both representations include implicit entry inputs in the inferred register limit and retain unsigned M0, inactive-write and DPP update rules; wave32/wave64 native Vulkan readbacks cover relative reads, writes, split-offset swaps, inactive writes, DPP masks and out-of-range accesses, but console comparison and the performance cost of selecting over this span are unverified.
- [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; in the shaders seen they are metadata, and a caller that reads its program counter, which could address them, 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.
- [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.
- [s_call_b64](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation.