Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fe41032
fix(recompiler): give vertex-path shaders the NGG group info
RINNECODER Oct 14, 2026
ebf2219
fix(shader): put the compute wave index in TG_SIZE[11:6]
RINNECODER Oct 14, 2026
c14c0a4
fix(shader): take the 32_AR export alpha from channel 1 as GFX10 does
RINNECODER Oct 14, 2026
1af6ee8
fix(shader): order buffer and image memory at s_barrier
RINNECODER Oct 14, 2026
13f4ec6
fix(shader): decode GFX10 scratch offsets as one dword
RINNECODER Oct 14, 2026
1c69a74
fix(shader): accept MIMG sampler tuples ending in VCC
RINNECODER Oct 14, 2026
032b281
fix(recompiler): name the attribute of an out-of-range interpolation …
RINNECODER Oct 14, 2026
f0805aa
fix(shader): decode the high-half result of v_interp_p2_f16
RINNECODER Oct 14, 2026
32ccc90
fix(shader): clear the bits of inactive lanes in VALU carry-out and v…
RINNECODER Oct 14, 2026
a5fdb33
fix(shader): apply the f32 output modifier and denormal flush to v_cv…
RINNECODER Oct 14, 2026
325fe5f
fix(shader): update per-invocation LDS without atomics
RINNECODER Oct 14, 2026
5acb7d7
fix(shader): flush ds_add_f32 denormals and NaNs as the hardware does
RINNECODER Oct 14, 2026
5c715f3
fix(shader): run DS src2 lanes one at a time in lane order
RINNECODER Oct 14, 2026
00f5fcd
fix(agc): compare NOT_EQUAL unordered, reject TRUNC_COORD point sampl…
RINNECODER Oct 14, 2026
f054fa6
chore(shader): bump ShaderDiskCache FormatVersion to 35
RINNECODER Oct 14, 2026
e96f32d
test(shader): run ported upstream execution tests on Metal through a …
RINNECODER Oct 14, 2026
97daa9f
test(shader): run ported decoder and collector unit tests in the Meta…
RINNECODER Oct 14, 2026
42d8cf7
test(shader): check per-invocation LDS updates without atomics on the…
RINNECODER Oct 14, 2026
89dca28
test(shader): check the TG_SIZE wave index on Metal
RINNECODER Oct 14, 2026
3e1491a
test(shader): run the s_barrier memory-order execution test on Metal
RINNECODER Oct 14, 2026
ac62db0
test(shader): draw a 32_AR export on Metal
RINNECODER Oct 14, 2026
9f6506d
test(shader): draw an NGG vertex shader that reads the group info
RINNECODER Oct 14, 2026
2a9c1a6
test(shader): reject TRUNC_COORD point sampling in the color compare …
RINNECODER Oct 14, 2026
c131503
test(shader): run the src2 lane-order test without the Metal debug layer
RINNECODER Oct 14, 2026
ef1df5c
Merge remote-tracking branch 'origin/main' into upr1/shader-semantics
RINNECODER Oct 14, 2026
4b1a956
docs(shader): port upstream debt entries and add the FormatVersion 35…
RINNECODER Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions core/libs/prx/libSceAgcDriver/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1517,6 +1517,14 @@ if(BUILD_TESTING)
set_tests_properties(pixel_ancillary PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(interpolation_reference_range_tests ${LIBS_INCLUDE_DIR}/tests/InterpolationReferenceRange.cpp)
target_link_libraries(interpolation_reference_range_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(interpolation_reference_range_tests)
add_test(NAME interpolation_reference_range COMMAND interpolation_reference_range_tests)
if(WIN32)
set_tests_properties(interpolation_reference_range PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(guest_indexed_draw_tests ${LIBS_INCLUDE_DIR}/tests/GuestIndexedDraw.cpp)
target_include_directories(guest_indexed_draw_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_indexed_draw_tests PRIVATE libSceAgc libSceAgcDriver libc)
Expand Down Expand Up @@ -1662,3 +1670,15 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()


if(BUILD_TESTING)
add_test_executable(agc_flat_scratch_tests tests/FlatScratch.cpp)
target_link_libraries(agc_flat_scratch_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(agc_flat_scratch_tests)
add_test(NAME agc_flat_scratch COMMAND agc_flat_scratch_tests)
set_tests_properties(agc_flat_scratch PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_flat_scratch PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
48 changes: 48 additions & 0 deletions core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include "Translation/TranslationContext.hpp"
#include "RdnaDecoder/RdnaMemoryOpDecoder.hpp"
#include <array>
#include <cstdio>
#include <stdexcept>
using namespace ShaderRecompiler;
static unsigned tests=0;
#define Require(value) do { if(!(value)){std::fprintf(stderr,"flat scratch assertion line %u: %s\n",__LINE__,#value);throw std::runtime_error("flat scratch regression");}} while(false)
static RdnaInstruction Decode(unsigned seg,unsigned saddr,unsigned addr,unsigned offset=0,unsigned opcode=12){
const std::array<unsigned,2> code{(0x37u<<26)|(opcode<<18)|(seg<<14)|offset,(2u<<24)|(saddr<<16)|addr};
return DecodeRdnaFlat(0x154cu,code,0);
}
static void Reject(unsigned seg,unsigned saddr,unsigned addr){try{(void)Decode(seg,saddr,addr);}catch(const std::runtime_error&){++tests;return;}throw std::runtime_error("invalid FLAT accepted");}
static void Scratch(unsigned saddr,unsigned addr,unsigned expectedVector,unsigned expectedScalar){
auto op=Decode(1,saddr,addr,0xffcu);
Require(op.memorySegment==1 && op.memoryOffset==-4 && op.sourceCount==2);
Require(op.source0.kind==(expectedVector?RdnaOperandKind::VectorRegister:RdnaOperandKind::IntegerInlineConstant));
Require(op.source1.kind==(expectedScalar?RdnaOperandKind::ScalarRegister:RdnaOperandKind::IntegerInlineConstant));
if(expectedVector)Require(op.source0.reg==addr);else Require(op.source0.value==0);
if(expectedScalar)Require(op.source1.reg==saddr);else Require(op.source1.value==0);
IrProgram program;auto& block=program.CreateBlock();program.SetEntryBlock(block);TranslationContext context(program,block,256);
context.TranslateInstruction(op);
unsigned vectors=0,addressReads=0,destinationReads=0,scalars=0,loads=0,scratch=0;
for(auto* inst:block.Instructions()){
if(inst->Opcode()==IrOpcode::GetVectorRegister){
++vectors;auto reg=inst->Arguments().at(0)->Register().index;Require(reg<256);
if(reg==addr)++addressReads;
if(reg==2)++destinationReads;
}
if(inst->Opcode()==IrOpcode::GetScalarRegister)++scalars;
if(inst->Opcode()==IrOpcode::LoadAddressU32)++loads;
if(inst->Opcode()==IrOpcode::GetScratchResource)++scratch;
}
std::printf("SV/SS/ST saddr=%u addr=%u vector=%u scalar=%u loads=%u scratch=%u\n",saddr,addr,vectors,scalars,loads,scratch);std::fflush(stdout);
Require(vectors==expectedVector+1 && addressReads==expectedVector && destinationReads==1 && scalars==expectedScalar && loads==1 && scratch==1);
++tests;
}
int main(){

const std::array<unsigned,2> code{0xdc304000u,0x027d00ffu};auto actual=DecodeRdnaFlat(0x154cu,code,0);
Require(actual.destination.reg==2 && actual.source0.reg==255 && actual.source1.value==0);++tests;
Scratch(125,255,1,0);Scratch(125,254,1,0);Scratch(0,255,0,1);Scratch(105,255,0,1);Scratch(127,255,0,0);
Reject(0,125,255);Reject(2,125,255);Reject(2,127,255);Reject(1,108,0);Reject(3,0,0);
auto flat=Decode(0,125,254);Require(flat.source1.kind==RdnaOperandKind::VectorRegister && flat.source1.reg==255);++tests;
auto global=Decode(2,0,255);Require(global.source0.reg==255 && global.source1.kind==RdnaOperandKind::ScalarRegister);++tests;
try{const std::array<unsigned,1> truncated{0xdc304000u};(void)DecodeRdnaFlat(0,truncated,0);throw std::logic_error("truncated accepted");}catch(const std::out_of_range&){++tests;}
std::printf("PASS %u flat scratch decode/IR and invalid boundary cases\n",tests);
}
76 changes: 76 additions & 0 deletions core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3258,6 +3258,81 @@ void verifyFunctionLdsBound() {
require(unsized == FunctionLdsDwordLimit, "function LDS: an access without a known width must keep the full array");
}

void verifyFunctionLdsUpdates() {
using namespace ShaderRecompiler;
static constexpr std::array<std::uint32_t, 5> capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityClipDistance};
const auto compile = [](ShaderStage stage, std::uint32_t waveSize, std::span<const std::uint32_t> body) {
std::vector<std::uint32_t> code{0x7e000280u, 0x7e020292u, 0xbefc03ffu, 0x0000ffffu};
code.insert(code.end(), body.begin(), body.end());
code.insert(code.end(), {0xbf8cc07fu, 0xd8d80000u, 0x02000000u, 0xbf8cc07fu, stage == ShaderStage::Fragment ? 0xf800180fu : 0xf80008cfu, 0x02020202u, 0xbf810000u});
RecompileRequest request{};
request.shader = {stage, 0x40000u, code, 0, {}};
request.context.waveSize = waveSize;
if (stage == ShaderStage::Fragment) {
ShaderPixelStageInfo info{};
info.wave32 = waveSize == 32u;
info.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter);
info.hasPerspectiveCenterVgpr = true;
info.targetOutputMode[0] = 9u;
info.targetExportMapping.fill(0xe4u);
request.context.pixel = info;
} else {
request.context.vertex = ShaderVertexStageInfo{};
}
request.target.vulkanVersion = 0x00401000u;
request.target.spirvVersion = 0x00010300u;
request.target.subgroupSize = 32u;
request.target.supportedCapabilities = capabilities;
request.layout.pushConstantSizeBytes = 128;
request.useCache = false;
return Recompile(request);
};
const auto violations = [](const std::vector<std::uint32_t>& words) {
std::map<std::uint32_t, std::uint32_t> storage;
std::map<std::uint32_t, std::uint32_t> constants;
std::map<std::uint32_t, std::uint32_t> types;
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
const auto op = static_cast<spv::Op>(words[at] & 0xffffu);
if (op == spv::OpTypePointer) storage[words[at + 1]] = words[at + 2];
if (op == spv::OpConstant && (words[at] >> 16u) == 4u) constants[words[at + 2]] = words[at + 3];
if (op == spv::OpVariable || op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain) types[words[at + 2]] = words[at + 1];
}
const auto workgroupScope = [&](std::uint32_t id) { return constants.count(id) != 0 && constants.at(id) == spv::ScopeWorkgroup; };
std::size_t found = 0;
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
const auto op = static_cast<spv::Op>(words[at] & 0xffffu);
if (op == spv::OpMemoryBarrier && workgroupScope(words[at + 1])) ++found;
if ((op < spv::OpAtomicLoad || op > spv::OpAtomicXor) && op != spv::OpAtomicFMinEXT && op != spv::OpAtomicFMaxEXT && op != spv::OpAtomicFAddEXT) continue;
const auto pointer = words[at + (op == spv::OpAtomicStore ? 1u : 3u)];
const auto type = types.find(pointer);
if (type != types.end() && storage.count(type->second) != 0 && storage.at(type->second) == spv::StorageClassFunction) ++found;
if (workgroupScope(words[at + (op == spv::OpAtomicStore ? 2u : 4u)])) ++found;
}
return found;
};
using Program = std::pair<const char*, std::array<std::uint32_t, 2>>;
static constexpr std::array<Program, 8> programs{{
{"ds_write_b8", {0xd8780000u, 0x00000100u}},
{"ds_write_b16", {0xd87c0000u, 0x00000100u}},
{"ds_write_b8_d16_hi", {0xda800000u, 0x00000100u}},
{"ds_write_b16_d16_hi", {0xda840000u, 0x00000100u}},
{"ds_add_u32", {0xd8000000u, 0x00000100u}},
{"ds_inc_rtn_u32", {0xd88c0000u, 0x03000100u}},
{"ds_mskor_b32", {0xd8300000u, 0x00010100u}},
{"ds_append", {0xd8f80000u, 0x03000000u}},
}};
for (const auto& [name, body] : programs) {
for (const auto stage : {ShaderStage::Fragment, ShaderStage::Vertex}) {
for (const auto waveSize : {64u, 32u}) {
const auto what = std::string("function LDS: ") + name + (stage == ShaderStage::Fragment ? " in a pixel" : " in a vertex") + " wave" + std::to_string(waveSize) + " program";
const auto result = compile(stage, waveSize, body);
require(!result.spirv.empty(), (what + " did not compile").c_str());
require(violations(result.spirv.Words()) == 0, (what + " uses an atomic on a Function pointer or a Workgroup memory scope").c_str());
}
}
}
}

struct NestedRequest {
std::vector<std::uint32_t> code;
std::array<std::uint32_t, 2> userData{};
Expand Down Expand Up @@ -3563,6 +3638,7 @@ int main(int argc, char** argv) {
verifyTwoLaneUniformValues();
verifyBdaReadFallbackFunctions();
verifyFunctionLdsBound();
verifyFunctionLdsUpdates();
verifyGuardedNullPointers();
verifyCubeDepthCompareMode();
#if ANYPS5_ENABLE_SPIRV_TOOLS
Expand Down
122 changes: 122 additions & 0 deletions core/libs/prx/libSceAgcDriver/tests/execution/BarrierGlobalMemory.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <spirv/unified1/spirv.hpp>
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <iostream>
#include <map>
#include <span>
#include <string>
#include <vector>

namespace {

using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;

constexpr std::uint32_t Threads = 128;
constexpr std::uint32_t Groups = 64;
constexpr std::uint32_t Rounds = 4;
constexpr std::uint32_t Sentinel = 0xdeadbeefu;
alignas(256) std::array<std::uint32_t, Threads * Groups> Exchange{};
alignas(256) std::array<std::uint32_t, Threads * Groups> Output{};

alignas(256) constexpr std::array<std::uint32_t, 16> ExchangeCode{
0x34020082u, 0x4c0400ffu, 0x0000007fu, 0x34040482u, 0x8f098908u, 0x4a060009u, 0xe0705000u, 0x09000301u,
0xbbfd0000u, 0xbf8a0000u, 0xe0305000u, 0x09000402u, 0xbf8c3f70u, 0xe0701000u, 0x09010401u, 0xbf810000u,
};

std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
}

std::string Hex(std::uint32_t value) {
char text[16];
std::snprintf(text, sizeof(text), "0x%08x", value);
return text;
}

ShaderRecompiler::RecompileResult Translate(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) {
std::vector<std::uint32_t> userData(8, 0u);
const auto exchange = BufferDescriptor(Exchange.data(), static_cast<std::uint32_t>(Exchange.size() * 4u));
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
std::copy(exchange.begin(), exchange.end(), userData.begin());
std::copy(output.begin(), output.end(), userData.begin() + 4);
const std::span<const std::uint32_t> code(ExchangeCode);
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {true, false, false}, false, 1};
ShaderRecompiler::RecompileRequest request{
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
{waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory},
device.Target(),
{0, 0, 0, 128}
};
request.useCache = false;
return ShaderRecompiler::Recompile(request);
}

void CheckBarrierSemantics(const ShaderRecompiler::RecompileResult& result, std::uint32_t waveSize) {
const std::span<const std::uint32_t> words(result.spirv.data(), result.spirv.size());
const std::string wave = "wave" + std::to_string(waveSize) + " ";
std::map<std::uint32_t, std::uint32_t> constants;
std::vector<std::array<std::uint32_t, 3>> barriers;
for (std::size_t cursor = 5; cursor < words.size();) {
const auto count = words[cursor] >> 16u;
const auto opcode = words[cursor] & 0xffffu;
Require(count != 0 && count <= words.size() - cursor, wave + "barrier global memory: truncated SPIR-V instruction");
if (opcode == spv::OpConstant && count == 4u) constants[words[cursor + 2]] = words[cursor + 3];
if (opcode == spv::OpControlBarrier && count == 4u) barriers.push_back({words[cursor + 1], words[cursor + 2], words[cursor + 3]});
cursor += count;
}
Require(!barriers.empty(), wave + "s_barrier emitted no OpControlBarrier");
const auto value = [&](std::uint32_t id) {
const auto found = constants.find(id);
Require(found != constants.end(), wave + "an OpControlBarrier operand is not a 32-bit constant");
return found->second;
};
const std::uint32_t required = spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsUniformMemoryMask | spv::MemorySemanticsWorkgroupMemoryMask | spv::MemorySemanticsImageMemoryMask;
for (const auto& [execution, scope, semantics] : barriers) {
Require(value(execution) == spv::ScopeWorkgroup && value(scope) == spv::ScopeWorkgroup, wave + "s_barrier is not a workgroup barrier over workgroup memory scope");
Require((value(semantics) & required) == required, wave + "s_barrier semantics " + Hex(value(semantics)) + " do not order buffer, workgroup and image memory with acquire-release (" + Hex(required) + ")");
}
}

void Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& result, std::uint32_t waveSize, std::uint32_t round) {
Exchange.fill(Sentinel);
Output.fill(Sentinel);
device.Dispatch(result, Groups, 1, 1, {}, reinterpret_cast<std::uintptr_t>(ExchangeCode.data()));
device.WaitIdle();
for (std::uint32_t group = 0; group < Groups; ++group) {
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
const std::uint32_t index = group * Threads + tid;
const std::uint32_t expected = group * Threads * 4u + (Threads - 1u - tid);
const std::string what = "wave" + std::to_string(waveSize) + " round " + std::to_string(round) + " group " + std::to_string(group) + " thread " + std::to_string(tid);
Require(Exchange[index] == group * Threads * 4u + tid, what + " stored " + Hex(Exchange[index]));
Require(Output[index] == expected, what + " read " + Hex(Output[index]) + " after s_barrier, expected " + Hex(expected));
}
}
}

}

int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
for (const std::uint32_t waveSize : {32u, 64u}) {
const auto result = Translate(*device, waveSize);
for (std::uint32_t round = 0; round < Rounds; ++round) Run(*device, result, waveSize, round);
CheckBarrierSemantics(result, waveSize);
}
std::puts("barrier global memory tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
Loading
Loading